@salesforce/afv-skills 1.14.0 → 1.16.0

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.
Files changed (365) hide show
  1. package/package.json +1 -1
  2. package/skills/activating-datacloud/SKILL.md +0 -1
  3. package/skills/analyzing-omnistudio-dependencies/SKILL.md +0 -1
  4. package/skills/applying-slds/SKILL.md +322 -0
  5. package/skills/applying-slds/checklists.md +83 -0
  6. package/skills/applying-slds/examples.md +283 -0
  7. package/skills/applying-slds/guidance/README.md +83 -0
  8. package/skills/applying-slds/guidance/blueprints-index.md +213 -0
  9. package/skills/applying-slds/guidance/icons-guidance.md +186 -0
  10. package/skills/applying-slds/guidance/overviews/borders.md +236 -0
  11. package/skills/applying-slds/guidance/overviews/color.md +266 -0
  12. package/skills/applying-slds/guidance/overviews/display-density.md +366 -0
  13. package/skills/applying-slds/guidance/overviews/icons.md +240 -0
  14. package/skills/applying-slds/guidance/overviews/illustrations.md +235 -0
  15. package/skills/applying-slds/guidance/overviews/shadows.md +176 -0
  16. package/skills/applying-slds/guidance/overviews/spacing.md +216 -0
  17. package/skills/applying-slds/guidance/overviews/typography.md +323 -0
  18. package/skills/applying-slds/guidance/overviews/utilities.md +542 -0
  19. package/skills/applying-slds/guidance/slds-development-guide.md +288 -0
  20. package/skills/applying-slds/guidance/styling-hooks/borders.md +202 -0
  21. package/skills/applying-slds/guidance/styling-hooks/color/expressive-palette-hooks.md +153 -0
  22. package/skills/applying-slds/guidance/styling-hooks/color/index.md +171 -0
  23. package/skills/applying-slds/guidance/styling-hooks/color/semantic/accent-hooks.md +204 -0
  24. package/skills/applying-slds/guidance/styling-hooks/color/semantic/feedback-hooks.md +768 -0
  25. package/skills/applying-slds/guidance/styling-hooks/color/semantic/surface-hooks.md +337 -0
  26. package/skills/applying-slds/guidance/styling-hooks/color/system-hooks.md +132 -0
  27. package/skills/applying-slds/guidance/styling-hooks/index.md +327 -0
  28. package/skills/applying-slds/guidance/styling-hooks/shadows.md +238 -0
  29. package/skills/applying-slds/guidance/styling-hooks/spacing.md +254 -0
  30. package/skills/applying-slds/guidance/styling-hooks/typography.md +448 -0
  31. package/skills/applying-slds/guidance/utilities/alignment.md +119 -0
  32. package/skills/applying-slds/guidance/utilities/borders.md +131 -0
  33. package/skills/applying-slds/guidance/utilities/box.md +125 -0
  34. package/skills/applying-slds/guidance/utilities/color.md +165 -0
  35. package/skills/applying-slds/guidance/utilities/dark-mode.md +111 -0
  36. package/skills/applying-slds/guidance/utilities/description-list.md +168 -0
  37. package/skills/applying-slds/guidance/utilities/floats.md +117 -0
  38. package/skills/applying-slds/guidance/utilities/grid.md +264 -0
  39. package/skills/applying-slds/guidance/utilities/horizontal-list.md +110 -0
  40. package/skills/applying-slds/guidance/utilities/hyphenation.md +84 -0
  41. package/skills/applying-slds/guidance/utilities/index.md +205 -0
  42. package/skills/applying-slds/guidance/utilities/interactions.md +89 -0
  43. package/skills/applying-slds/guidance/utilities/layout.md +109 -0
  44. package/skills/applying-slds/guidance/utilities/line-clamp.md +131 -0
  45. package/skills/applying-slds/guidance/utilities/margin.md +155 -0
  46. package/skills/applying-slds/guidance/utilities/media-object.md +161 -0
  47. package/skills/applying-slds/guidance/utilities/name-value-list.md +152 -0
  48. package/skills/applying-slds/guidance/utilities/padding.md +155 -0
  49. package/skills/applying-slds/guidance/utilities/position.md +177 -0
  50. package/skills/applying-slds/guidance/utilities/print.md +114 -0
  51. package/skills/applying-slds/guidance/utilities/scrollable.md +126 -0
  52. package/skills/applying-slds/guidance/utilities/sizing.md +190 -0
  53. package/skills/applying-slds/guidance/utilities/themes.md +121 -0
  54. package/skills/applying-slds/guidance/utilities/truncate.md +127 -0
  55. package/skills/applying-slds/guidance/utilities/typography.md +166 -0
  56. package/skills/applying-slds/guidance/utilities/vertical-list.md +166 -0
  57. package/skills/applying-slds/guidance/utilities/visibility.md +228 -0
  58. package/skills/applying-slds/metadata/README.md +84 -0
  59. package/skills/applying-slds/metadata/blueprints/components/accordion.yaml +304 -0
  60. package/skills/applying-slds/metadata/blueprints/components/activity-timeline.yaml +92 -0
  61. package/skills/applying-slds/metadata/blueprints/components/alert.yaml +103 -0
  62. package/skills/applying-slds/metadata/blueprints/components/app-launcher.yaml +94 -0
  63. package/skills/applying-slds/metadata/blueprints/components/avatar-group.yaml +81 -0
  64. package/skills/applying-slds/metadata/blueprints/components/avatar.yaml +97 -0
  65. package/skills/applying-slds/metadata/blueprints/components/badges.yaml +102 -0
  66. package/skills/applying-slds/metadata/blueprints/components/brand-band.yaml +198 -0
  67. package/skills/applying-slds/metadata/blueprints/components/breadcrumbs.yaml +95 -0
  68. package/skills/applying-slds/metadata/blueprints/components/builder-header.yaml +192 -0
  69. package/skills/applying-slds/metadata/blueprints/components/button-groups.yaml +82 -0
  70. package/skills/applying-slds/metadata/blueprints/components/button-icons.yaml +295 -0
  71. package/skills/applying-slds/metadata/blueprints/components/buttons.yaml +230 -0
  72. package/skills/applying-slds/metadata/blueprints/components/cards.yaml +124 -0
  73. package/skills/applying-slds/metadata/blueprints/components/carousel.yaml +140 -0
  74. package/skills/applying-slds/metadata/blueprints/components/chat.yaml +179 -0
  75. package/skills/applying-slds/metadata/blueprints/components/checkbox-button-group.yaml +192 -0
  76. package/skills/applying-slds/metadata/blueprints/components/checkbox-button.yaml +204 -0
  77. package/skills/applying-slds/metadata/blueprints/components/checkbox-toggle.yaml +177 -0
  78. package/skills/applying-slds/metadata/blueprints/components/checkbox.yaml +108 -0
  79. package/skills/applying-slds/metadata/blueprints/components/color-picker.yaml +172 -0
  80. package/skills/applying-slds/metadata/blueprints/components/combobox.yaml +136 -0
  81. package/skills/applying-slds/metadata/blueprints/components/counter.yaml +147 -0
  82. package/skills/applying-slds/metadata/blueprints/components/data-tables.yaml +157 -0
  83. package/skills/applying-slds/metadata/blueprints/components/datepickers.yaml +130 -0
  84. package/skills/applying-slds/metadata/blueprints/components/datetime-picker.yaml +155 -0
  85. package/skills/applying-slds/metadata/blueprints/components/docked-composer.yaml +201 -0
  86. package/skills/applying-slds/metadata/blueprints/components/docked-form-footer.yaml +161 -0
  87. package/skills/applying-slds/metadata/blueprints/components/docked-utility-bar.yaml +175 -0
  88. package/skills/applying-slds/metadata/blueprints/components/drop-zone.yaml +115 -0
  89. package/skills/applying-slds/metadata/blueprints/components/dueling-picklist.yaml +196 -0
  90. package/skills/applying-slds/metadata/blueprints/components/dynamic-icons.yaml +128 -0
  91. package/skills/applying-slds/metadata/blueprints/components/dynamic-menu.yaml +141 -0
  92. package/skills/applying-slds/metadata/blueprints/components/expandable-section.yaml +115 -0
  93. package/skills/applying-slds/metadata/blueprints/components/expression.yaml +143 -0
  94. package/skills/applying-slds/metadata/blueprints/components/feeds.yaml +125 -0
  95. package/skills/applying-slds/metadata/blueprints/components/file-selector.yaml +154 -0
  96. package/skills/applying-slds/metadata/blueprints/components/files.yaml +119 -0
  97. package/skills/applying-slds/metadata/blueprints/components/form-element.yaml +145 -0
  98. package/skills/applying-slds/metadata/blueprints/components/global-header.yaml +120 -0
  99. package/skills/applying-slds/metadata/blueprints/components/global-navigation.yaml +100 -0
  100. package/skills/applying-slds/metadata/blueprints/components/icons.yaml +138 -0
  101. package/skills/applying-slds/metadata/blueprints/components/illustration.yaml +205 -0
  102. package/skills/applying-slds/metadata/blueprints/components/input.yaml +151 -0
  103. package/skills/applying-slds/metadata/blueprints/components/list-builder.yaml +127 -0
  104. package/skills/applying-slds/metadata/blueprints/components/lookups.yaml +132 -0
  105. package/skills/applying-slds/metadata/blueprints/components/map.yaml +118 -0
  106. package/skills/applying-slds/metadata/blueprints/components/menus.yaml +134 -0
  107. package/skills/applying-slds/metadata/blueprints/components/modals.yaml +152 -0
  108. package/skills/applying-slds/metadata/blueprints/components/notifications.yaml +88 -0
  109. package/skills/applying-slds/metadata/blueprints/components/page-headers.yaml +135 -0
  110. package/skills/applying-slds/metadata/blueprints/components/panels.yaml +149 -0
  111. package/skills/applying-slds/metadata/blueprints/components/path.yaml +154 -0
  112. package/skills/applying-slds/metadata/blueprints/components/picklist.yaml +125 -0
  113. package/skills/applying-slds/metadata/blueprints/components/pills.yaml +154 -0
  114. package/skills/applying-slds/metadata/blueprints/components/popovers.yaml +120 -0
  115. package/skills/applying-slds/metadata/blueprints/components/progress-bar.yaml +110 -0
  116. package/skills/applying-slds/metadata/blueprints/components/progress-indicator.yaml +133 -0
  117. package/skills/applying-slds/metadata/blueprints/components/progress-ring.yaml +102 -0
  118. package/skills/applying-slds/metadata/blueprints/components/prompt.yaml +126 -0
  119. package/skills/applying-slds/metadata/blueprints/components/publishers.yaml +178 -0
  120. package/skills/applying-slds/metadata/blueprints/components/radio-button-group.yaml +172 -0
  121. package/skills/applying-slds/metadata/blueprints/components/radio-group.yaml +112 -0
  122. package/skills/applying-slds/metadata/blueprints/components/rich-text-editor.yaml +135 -0
  123. package/skills/applying-slds/metadata/blueprints/components/scoped-notifications.yaml +188 -0
  124. package/skills/applying-slds/metadata/blueprints/components/scoped-tabs.yaml +97 -0
  125. package/skills/applying-slds/metadata/blueprints/components/select.yaml +127 -0
  126. package/skills/applying-slds/metadata/blueprints/components/setup-assistant.yaml +152 -0
  127. package/skills/applying-slds/metadata/blueprints/components/slider.yaml +111 -0
  128. package/skills/applying-slds/metadata/blueprints/components/spinners.yaml +135 -0
  129. package/skills/applying-slds/metadata/blueprints/components/split-view.yaml +112 -0
  130. package/skills/applying-slds/metadata/blueprints/components/summary-detail.yaml +103 -0
  131. package/skills/applying-slds/metadata/blueprints/components/tabs.yaml +138 -0
  132. package/skills/applying-slds/metadata/blueprints/components/textarea.yaml +116 -0
  133. package/skills/applying-slds/metadata/blueprints/components/tiles.yaml +108 -0
  134. package/skills/applying-slds/metadata/blueprints/components/timepicker.yaml +111 -0
  135. package/skills/applying-slds/metadata/blueprints/components/toast.yaml +154 -0
  136. package/skills/applying-slds/metadata/blueprints/components/tooltips.yaml +107 -0
  137. package/skills/applying-slds/metadata/blueprints/components/tree-grid.yaml +116 -0
  138. package/skills/applying-slds/metadata/blueprints/components/trees.yaml +116 -0
  139. package/skills/applying-slds/metadata/blueprints/components/trial-bar.yaml +112 -0
  140. package/skills/applying-slds/metadata/blueprints/components/vertical-navigation.yaml +130 -0
  141. package/skills/applying-slds/metadata/blueprints/components/vertical-tabs.yaml +140 -0
  142. package/skills/applying-slds/metadata/blueprints/components/visual-picker.yaml +150 -0
  143. package/skills/applying-slds/metadata/blueprints/components/welcome-mat.yaml +136 -0
  144. package/skills/applying-slds/metadata/hooks-index.json +6272 -0
  145. package/skills/applying-slds/metadata/icon-metadata.json +38466 -0
  146. package/skills/applying-slds/metadata/utilities-index.json +21912 -0
  147. package/skills/applying-slds/references/component-selection.md +112 -0
  148. package/skills/applying-slds/references/icons-decision-guide.md +124 -0
  149. package/skills/applying-slds/references/styling-decision-guide.md +228 -0
  150. package/skills/applying-slds/references/utilities-quick-ref.md +125 -0
  151. package/skills/applying-slds/scripts/search-blueprints.cjs +117 -0
  152. package/skills/applying-slds/scripts/search-hooks.cjs +139 -0
  153. package/skills/applying-slds/scripts/search-icons.cjs +174 -0
  154. package/skills/applying-slds/scripts/search-utilities.cjs +161 -0
  155. package/skills/building-mobile-apps/SKILL.md +0 -1
  156. package/skills/building-omnistudio-callable-apex/SKILL.md +0 -1
  157. package/skills/building-omnistudio-datamapper/SKILL.md +0 -1
  158. package/skills/building-omnistudio-flexcard/SKILL.md +0 -1
  159. package/skills/building-omnistudio-integration-procedure/SKILL.md +0 -1
  160. package/skills/building-omnistudio-omniscript/SKILL.md +0 -1
  161. package/skills/building-sf-integrations/SKILL.md +0 -1
  162. package/skills/configuring-connected-apps/SKILL.md +0 -1
  163. package/skills/connecting-datacloud/SKILL.md +0 -1
  164. package/skills/creating-b2b-commerce-store/SKILL.md +0 -1
  165. package/skills/debugging-apex-logs/SKILL.md +0 -1
  166. package/skills/deploying-metadata/SKILL.md +0 -1
  167. package/skills/deploying-omnistudio-datapacks/SKILL.md +0 -1
  168. package/skills/developing-agentforce/SKILL.md +0 -1
  169. package/skills/fetching-salesforce-docs/SKILL.md +0 -1
  170. package/skills/generating-custom-lightning-type/SKILL.md +17 -39
  171. package/skills/generating-custom-lightning-type/assets/primitive-types-and-constraints.md +41 -0
  172. package/skills/generating-custom-lightning-type/references/widget-rendition.md +124 -0
  173. package/skills/generating-lwc-components/SKILL.md +0 -1
  174. package/skills/generating-mermaid-diagrams/SKILL.md +0 -1
  175. package/skills/generating-visual-diagrams/SKILL.md +0 -1
  176. package/skills/handling-sf-data/SKILL.md +0 -1
  177. package/skills/harmonizing-datacloud/SKILL.md +0 -1
  178. package/skills/integrating-b2b-commerce-open-code-components/SKILL.md +0 -1
  179. package/skills/investigating-agentforce-architecture/README.md +156 -0
  180. package/skills/investigating-agentforce-architecture/SKILL.md +230 -0
  181. package/skills/investigating-agentforce-architecture/assets/cli/describe_sobject.yaml +16 -0
  182. package/skills/investigating-agentforce-architecture/assets/cli/describe_tooling_sobject.yaml +17 -0
  183. package/skills/investigating-agentforce-architecture/assets/cli/list_metadata_genaiprompttemplate.yaml +17 -0
  184. package/skills/investigating-agentforce-architecture/assets/cli/org_display.yaml +15 -0
  185. package/skills/investigating-agentforce-architecture/assets/cli/retrieve_genai_plugin.yaml +18 -0
  186. package/skills/investigating-agentforce-architecture/assets/cli/show_access_token.yaml +27 -0
  187. package/skills/investigating-agentforce-architecture/assets/mermaid/action_tree.mmd +20 -0
  188. package/skills/investigating-agentforce-architecture/assets/mermaid/data_flow.mmd +19 -0
  189. package/skills/investigating-agentforce-architecture/assets/mermaid/dependency_graph.mmd +19 -0
  190. package/skills/investigating-agentforce-architecture/assets/mermaid/invocation_sequence.mmd +20 -0
  191. package/skills/investigating-agentforce-architecture/assets/mermaid/planner_state.mmd +18 -0
  192. package/skills/investigating-agentforce-architecture/assets/soql/apex_class_bodies_by_ids.soql +3 -0
  193. package/skills/investigating-agentforce-architecture/assets/soql/apex_class_bodies_by_names.soql +3 -0
  194. package/skills/investigating-agentforce-architecture/assets/soql/bot_definition_details.soql +3 -0
  195. package/skills/investigating-agentforce-architecture/assets/soql/bot_version_lookup.soql +4 -0
  196. package/skills/investigating-agentforce-architecture/assets/soql/flow_definition_by_ids.soql +3 -0
  197. package/skills/investigating-agentforce-architecture/assets/soql/flow_definition_ids_by_names.soql +3 -0
  198. package/skills/investigating-agentforce-architecture/assets/soql/flow_definition_view_by_durable_ids.soql +4 -0
  199. package/skills/investigating-agentforce-architecture/assets/soql/flow_metadata_by_id.soql +3 -0
  200. package/skills/investigating-agentforce-architecture/assets/soql/functions_by_plugins.soql +5 -0
  201. package/skills/investigating-agentforce-architecture/assets/soql/planner_attrs_by_parent_ids.soql +3 -0
  202. package/skills/investigating-agentforce-architecture/assets/soql/planner_bundle_functions.soql +3 -0
  203. package/skills/investigating-agentforce-architecture/assets/soql/planner_definition_by_agent_chain.soql +3 -0
  204. package/skills/investigating-agentforce-architecture/assets/soql/plugin_functions_by_plugin_ids.soql +3 -0
  205. package/skills/investigating-agentforce-architecture/assets/soql/plugin_instructions_by_plugin_ids.soql +3 -0
  206. package/skills/investigating-agentforce-architecture/assets/soql/plugins_by_planner.soql +4 -0
  207. package/skills/investigating-agentforce-architecture/references/architecture_sections.md +243 -0
  208. package/skills/investigating-agentforce-architecture/references/contract.json +244 -0
  209. package/skills/investigating-agentforce-architecture/references/soql_fields.md +512 -0
  210. package/skills/investigating-agentforce-architecture/scripts/_shared/__init__.py +1 -0
  211. package/skills/investigating-agentforce-architecture/scripts/_shared/fs_guard.py +329 -0
  212. package/skills/investigating-agentforce-architecture/scripts/_shared/paths.py +110 -0
  213. package/skills/investigating-agentforce-architecture/scripts/_shared/runtime.py +59 -0
  214. package/skills/investigating-agentforce-architecture/scripts/_shared/sql.py +10 -0
  215. package/skills/investigating-agentforce-architecture/scripts/cache_check.py +234 -0
  216. package/skills/investigating-agentforce-architecture/scripts/config.py +131 -0
  217. package/skills/investigating-agentforce-architecture/scripts/fetch_soql.py +689 -0
  218. package/skills/investigating-agentforce-architecture/scripts/finalize.py +295 -0
  219. package/skills/investigating-agentforce-architecture/scripts/main.py +2835 -0
  220. package/skills/investigating-agentforce-architecture/scripts/metadata_listing.py +265 -0
  221. package/skills/investigating-agentforce-architecture/scripts/parallel_retrieve.py +69 -0
  222. package/skills/investigating-agentforce-architecture/scripts/parse_bundle.py +215 -0
  223. package/skills/investigating-agentforce-architecture/scripts/parse_wave.py +845 -0
  224. package/skills/investigating-agentforce-architecture/scripts/probe_channels.py +302 -0
  225. package/skills/investigating-agentforce-architecture/scripts/render_architecture.py +1043 -0
  226. package/skills/investigating-agentforce-architecture/scripts/resolve_bot.py +255 -0
  227. package/skills/investigating-agentforce-architecture/scripts/resolve_invocation_target.py +130 -0
  228. package/skills/investigating-agentforce-architecture/scripts/rest_client.py +763 -0
  229. package/skills/investigating-agentforce-architecture/scripts/retrieve_planner.py +13 -0
  230. package/skills/investigating-agentforce-architecture/scripts/sf_cli.py +242 -0
  231. package/skills/investigating-agentforce-architecture/scripts/soql_loader.py +253 -0
  232. package/skills/investigating-agentforce-architecture/scripts/summarize_tree.py +143 -0
  233. package/skills/investigating-agentforce-architecture/scripts/tests/__init__.py +0 -0
  234. package/skills/investigating-agentforce-architecture/scripts/tests/_bootstrap.py +23 -0
  235. package/skills/investigating-agentforce-architecture/scripts/tests/fixtures/__init__.py +0 -0
  236. package/skills/investigating-agentforce-architecture/scripts/tests/fixtures/genai_payloads.py +400 -0
  237. package/skills/investigating-agentforce-architecture/scripts/tests/test_cache_check.py +307 -0
  238. package/skills/investigating-agentforce-architecture/scripts/tests/test_cache_check_main.py +283 -0
  239. package/skills/investigating-agentforce-architecture/scripts/tests/test_config.py +115 -0
  240. package/skills/investigating-agentforce-architecture/scripts/tests/test_end_to_end_fixture.py +651 -0
  241. package/skills/investigating-agentforce-architecture/scripts/tests/test_finalize.py +278 -0
  242. package/skills/investigating-agentforce-architecture/scripts/tests/test_flow_children_inflation.py +582 -0
  243. package/skills/investigating-agentforce-architecture/scripts/tests/test_fs_guard.py +113 -0
  244. package/skills/investigating-agentforce-architecture/scripts/tests/test_iterative_wave_b.py +478 -0
  245. package/skills/investigating-agentforce-architecture/scripts/tests/test_main_pipeline.py +3359 -0
  246. package/skills/investigating-agentforce-architecture/scripts/tests/test_parallel_retrieve.py +131 -0
  247. package/skills/investigating-agentforce-architecture/scripts/tests/test_parse_bundle.py +400 -0
  248. package/skills/investigating-agentforce-architecture/scripts/tests/test_parse_wave.py +644 -0
  249. package/skills/investigating-agentforce-architecture/scripts/tests/test_parse_wave_classifiers.py +224 -0
  250. package/skills/investigating-agentforce-architecture/scripts/tests/test_parse_wave_helpers.py +380 -0
  251. package/skills/investigating-agentforce-architecture/scripts/tests/test_parse_wave_main.py +397 -0
  252. package/skills/investigating-agentforce-architecture/scripts/tests/test_per_branch_visited.py +244 -0
  253. package/skills/investigating-agentforce-architecture/scripts/tests/test_probe_channels.py +359 -0
  254. package/skills/investigating-agentforce-architecture/scripts/tests/test_probe_cli_recipes.py +185 -0
  255. package/skills/investigating-agentforce-architecture/scripts/tests/test_render_architecture.py +810 -0
  256. package/skills/investigating-agentforce-architecture/scripts/tests/test_resolve_bot.py +203 -0
  257. package/skills/investigating-agentforce-architecture/scripts/tests/test_resolve_creds.py +157 -0
  258. package/skills/investigating-agentforce-architecture/scripts/tests/test_resolve_invocation_target.py +145 -0
  259. package/skills/investigating-agentforce-architecture/scripts/tests/test_rest_client.py +1253 -0
  260. package/skills/investigating-agentforce-architecture/scripts/tests/test_runtime_override.py +100 -0
  261. package/skills/investigating-agentforce-architecture/scripts/tests/test_sf_cli.py +261 -0
  262. package/skills/investigating-agentforce-architecture/scripts/tests/test_signature_stamping.py +466 -0
  263. package/skills/investigating-agentforce-architecture/scripts/tests/test_soql_loader.py +501 -0
  264. package/skills/investigating-agentforce-architecture/scripts/tests/test_summarize_tree.py +241 -0
  265. package/skills/investigating-agentforce-architecture/scripts/tests/test_write_emit_ctx.py +480 -0
  266. package/skills/investigating-agentforce-architecture/tools/emit_env.py +157 -0
  267. package/skills/investigating-agentforce-architecture/tools/emit_result.py +262 -0
  268. package/skills/investigating-agentforce-architecture/tools/sanitize.py +33 -0
  269. package/skills/investigating-agentforce-architecture/tools/write_emit_ctx.py +332 -0
  270. package/skills/investigating-agentforce-d360/README.md +123 -0
  271. package/skills/investigating-agentforce-d360/SKILL.md +163 -0
  272. package/skills/investigating-agentforce-d360/assets/dc/app_generation.sql +51 -0
  273. package/skills/investigating-agentforce-d360/assets/dc/content_category.sql +44 -0
  274. package/skills/investigating-agentforce-d360/assets/dc/content_quality.sql +41 -0
  275. package/skills/investigating-agentforce-d360/assets/dc/discover_sessions.sql +36 -0
  276. package/skills/investigating-agentforce-d360/assets/dc/feedback.sql +47 -0
  277. package/skills/investigating-agentforce-d360/assets/dc/feedback_details.sql +38 -0
  278. package/skills/investigating-agentforce-d360/assets/dc/gateway_records.sql +45 -0
  279. package/skills/investigating-agentforce-d360/assets/dc/gateway_request_llm.sql +50 -0
  280. package/skills/investigating-agentforce-d360/assets/dc/gateway_request_metadata.sql +44 -0
  281. package/skills/investigating-agentforce-d360/assets/dc/gateway_request_tags.sql +42 -0
  282. package/skills/investigating-agentforce-d360/assets/dc/gateway_requests.sql +89 -0
  283. package/skills/investigating-agentforce-d360/assets/dc/gateway_responses.sql +43 -0
  284. package/skills/investigating-agentforce-d360/assets/dc/generations.sql +52 -0
  285. package/skills/investigating-agentforce-d360/assets/dc/interactions.sql +53 -0
  286. package/skills/investigating-agentforce-d360/assets/dc/messages.sql +53 -0
  287. package/skills/investigating-agentforce-d360/assets/dc/messaging_session.sql +37 -0
  288. package/skills/investigating-agentforce-d360/assets/dc/moment_interactions.sql +34 -0
  289. package/skills/investigating-agentforce-d360/assets/dc/moments.sql +39 -0
  290. package/skills/investigating-agentforce-d360/assets/dc/participants.sql +48 -0
  291. package/skills/investigating-agentforce-d360/assets/dc/sessions.sql +78 -0
  292. package/skills/investigating-agentforce-d360/assets/dc/steps.sql +64 -0
  293. package/skills/investigating-agentforce-d360/assets/dc/tag_associations.sql +46 -0
  294. package/skills/investigating-agentforce-d360/assets/dc/tag_definition_associations.sql +37 -0
  295. package/skills/investigating-agentforce-d360/assets/dc/tag_definitions.sql +50 -0
  296. package/skills/investigating-agentforce-d360/assets/dc/tags.sql +37 -0
  297. package/skills/investigating-agentforce-d360/assets/dc/telemetry_spans.sql +55 -0
  298. package/skills/investigating-agentforce-d360/references/artifacts.md +50 -0
  299. package/skills/investigating-agentforce-d360/references/dc_dmo_fields.md +823 -0
  300. package/skills/investigating-agentforce-d360/references/dc_pipeline_contract.md +608 -0
  301. package/skills/investigating-agentforce-d360/scripts/_shared/__init__.py +2 -0
  302. package/skills/investigating-agentforce-d360/scripts/_shared/cli_override.py +98 -0
  303. package/skills/investigating-agentforce-d360/scripts/_shared/fs_guard.py +334 -0
  304. package/skills/investigating-agentforce-d360/scripts/_shared/paths.py +155 -0
  305. package/skills/investigating-agentforce-d360/scripts/_shared/runtime.py +59 -0
  306. package/skills/investigating-agentforce-d360/scripts/_shared/sql.py +14 -0
  307. package/skills/investigating-agentforce-d360/scripts/assemble_dc.py +1624 -0
  308. package/skills/investigating-agentforce-d360/scripts/config.py +45 -0
  309. package/skills/investigating-agentforce-d360/scripts/dc.py +188 -0
  310. package/skills/investigating-agentforce-d360/scripts/discover_sessions.py +556 -0
  311. package/skills/investigating-agentforce-d360/scripts/fetch_dc.py +1045 -0
  312. package/skills/investigating-agentforce-d360/scripts/render_dc.py +1750 -0
  313. package/skills/investigating-agentforce-d360/scripts/resolve_session.py +264 -0
  314. package/skills/investigating-agentforce-d360/scripts/storage.py +92 -0
  315. package/skills/investigating-agentforce-d360/scripts/tests/__init__.py +0 -0
  316. package/skills/investigating-agentforce-d360/scripts/tests/_bootstrap.py +15 -0
  317. package/skills/investigating-agentforce-d360/scripts/tests/fixtures/__init__.py +0 -0
  318. package/skills/investigating-agentforce-d360/scripts/tests/fixtures/synthetic_session.py +424 -0
  319. package/skills/investigating-agentforce-d360/scripts/tests/test_assemble_dc_bootstrap_and_mode.py +115 -0
  320. package/skills/investigating-agentforce-d360/scripts/tests/test_assemble_dc_gateway_direct.py +220 -0
  321. package/skills/investigating-agentforce-d360/scripts/tests/test_assemble_dc_gateway_direct_integration.py +158 -0
  322. package/skills/investigating-agentforce-d360/scripts/tests/test_assemble_dc_helpers.py +287 -0
  323. package/skills/investigating-agentforce-d360/scripts/tests/test_assemble_dc_integration.py +247 -0
  324. package/skills/investigating-agentforce-d360/scripts/tests/test_dc_and_resolve_session.py +433 -0
  325. package/skills/investigating-agentforce-d360/scripts/tests/test_discover_sessions.py +458 -0
  326. package/skills/investigating-agentforce-d360/scripts/tests/test_discover_sessions_grep_ci.py +193 -0
  327. package/skills/investigating-agentforce-d360/scripts/tests/test_fetch_dc_helpers.py +266 -0
  328. package/skills/investigating-agentforce-d360/scripts/tests/test_fetch_dc_identity.py +528 -0
  329. package/skills/investigating-agentforce-d360/scripts/tests/test_fetch_dc_main.py +251 -0
  330. package/skills/investigating-agentforce-d360/scripts/tests/test_fetch_dc_waterfall.py +229 -0
  331. package/skills/investigating-agentforce-d360/scripts/tests/test_fetch_dc_waterfall_full.py +283 -0
  332. package/skills/investigating-agentforce-d360/scripts/tests/test_identity_coherence.py +327 -0
  333. package/skills/investigating-agentforce-d360/scripts/tests/test_render_dc_branches.py +256 -0
  334. package/skills/investigating-agentforce-d360/scripts/tests/test_render_dc_gateway_direct.py +130 -0
  335. package/skills/investigating-agentforce-d360/scripts/tests/test_render_dc_helpers.py +291 -0
  336. package/skills/investigating-agentforce-d360/scripts/tests/test_render_dc_integration.py +220 -0
  337. package/skills/investigating-agentforce-d360/scripts/tests/test_render_dc_planner_llm_calls.py +284 -0
  338. package/skills/investigating-agentforce-d360/scripts/tests/test_render_dc_show_prompts_gating.py +215 -0
  339. package/skills/investigating-agentforce-d360/scripts/tests/test_resolve_from_disk.py +100 -0
  340. package/skills/investigating-agentforce-d360/scripts/tests/test_resolve_session_main.py +149 -0
  341. package/skills/investigating-agentforce-d360/scripts/tests/test_runtime_override.py +104 -0
  342. package/skills/investigating-agentforce-d360/scripts/tests/test_session_shape.py +95 -0
  343. package/skills/investigating-agentforce-d360/scripts/tests/test_session_shape_dropped_by_stdm.py +85 -0
  344. package/skills/managing-managed-event-subscription/SKILL.md +152 -0
  345. package/skills/managing-managed-event-subscription/assets/managed-event-subscription-template.xml +20 -0
  346. package/skills/managing-managed-event-subscription/references/delete-guide.md +57 -0
  347. package/skills/managing-managed-event-subscription/references/topic-name-formats.md +26 -0
  348. package/skills/managing-managed-event-subscription/references/update-constraints.md +30 -0
  349. package/skills/modeling-omnistudio-epc-catalog/SKILL.md +0 -1
  350. package/skills/observing-agentforce/SKILL.md +0 -1
  351. package/skills/orchestrating-datacloud/SKILL.md +0 -1
  352. package/skills/preparing-datacloud/SKILL.md +0 -1
  353. package/skills/querying-soql/SKILL.md +0 -1
  354. package/skills/retrieving-datacloud/SKILL.md +0 -1
  355. package/skills/running-apex-tests/SKILL.md +0 -1
  356. package/skills/running-code-analyzer/SKILL.md +0 -1
  357. package/skills/segmenting-datacloud/SKILL.md +0 -1
  358. package/skills/testing-agentforce/SKILL.md +0 -1
  359. package/skills/uplifting-components-to-slds2/SKILL.md +3 -2
  360. package/skills/uplifting-components-to-slds2/references/color-hooks-decision-guide.md +30 -9
  361. package/skills/uplifting-components-to-slds2/references/examples.md +24 -6
  362. package/skills/validating-slds/SKILL.md +262 -0
  363. package/skills/validating-slds/references/quality-checks.md +308 -0
  364. package/skills/validating-slds/references/report-format.md +302 -0
  365. package/skills/validating-slds/scripts/analyze-quality.cjs +521 -0
@@ -0,0 +1,763 @@
1
+ """Tooling + Data REST client scaffolding with security-critical primitives.
2
+
3
+ This module ships the HTTP primitives used by every REST-path caller in the
4
+ skill. Two security invariants are enforced here and must not be bypassed:
5
+
6
+ the Authorization header is STRIPPED from any cross-host redirect.
7
+ Python's default HTTPRedirectHandler blindly forwards all request headers
8
+ (including Authorization) to the redirect target. A compromised or
9
+ attacker-controlled edge that returns a 302 to an arbitrary host would
10
+ otherwise receive the bearer token. We subclass HTTPRedirectHandler to
11
+ strip Authorization whenever the redirect target hostname differs from
12
+ the original. Callers MUST use `build_opener()` — never `urllib.request.
13
+ urlopen` directly, which wires the default redirect handler.
14
+
15
+ access tokens MUST NEVER appear in exception strings, tracebacks,
16
+ or logged output. `redact_error(exc)` returns a safe string representation
17
+ with `Authorization: Bearer ...` scrubbed. Every `except` that surfaces
18
+ HTTP / subprocess error text runs the text through `redact_error` first.
19
+ Never call `log.exception()` on an object that carries request headers.
20
+ """
21
+ from __future__ import annotations
22
+
23
+ import email.utils
24
+ import functools
25
+ import json
26
+ import logging
27
+ import re
28
+ import time
29
+ import urllib.error
30
+ import urllib.parse
31
+ import urllib.request
32
+ from typing import Any, Callable, Tuple
33
+
34
+
35
+ # stdlib logger for transient-HTTP backoff breadcrumbs. The skill has
36
+ # no global logging config — we emit at DEBUG and leave handler/level wiring
37
+ # to callers. Absent any config, Python's "last-resort" handler suppresses
38
+ # DEBUG records, so this is a no-op in production until someone enables it.
39
+ logger = logging.getLogger(__name__)
40
+
41
+
42
+ class RestClientError(RuntimeError):
43
+ """REST request failed — always constructed with a redacted message."""
44
+
45
+
46
+ # -----------------------------------------------------------------------------
47
+ # error redaction
48
+ # -----------------------------------------------------------------------------
49
+ # Three patterns cover the bulk of token-leakage surfaces:
50
+ # 1. `Authorization: Bearer <token>` in any string context (header echo,
51
+ # exception repr, stringified HTTP error).
52
+ # 2. `accessToken=<token>` / `access_token=<token>` in URL-encoded bodies
53
+ # or query strings (`sf org display` errors sometimes echo these).
54
+ # 3. `"accessToken":"<token>"` or `"access_token":"<token>"` in JSON
55
+ # payload echoes.
56
+ #
57
+ # Regexes are intentionally permissive on the token character class
58
+ # (anything non-whitespace, non-quote, non-ampersand) — we'd rather
59
+ # over-redact than miss a token with an unexpected encoding.
60
+
61
+ _AUTH_HEADER_RE = re.compile(
62
+ r"(Authorization\s*:\s*Bearer\s+)\S+",
63
+ flags=re.IGNORECASE,
64
+ )
65
+ # Matches access[_]?Token=<value> in url-encoded form; stops at & or whitespace.
66
+ _ACCESS_TOKEN_QS_RE = re.compile(
67
+ r"(access[_]?token\s*=\s*)[^&\s\"']+",
68
+ flags=re.IGNORECASE,
69
+ )
70
+ # Matches "access[_]?Token":"<value>" in JSON; stops at closing quote.
71
+ _ACCESS_TOKEN_JSON_RE = re.compile(
72
+ r"(\"access[_]?token\"\s*:\s*\")[^\"]*",
73
+ flags=re.IGNORECASE,
74
+ )
75
+
76
+
77
+ def redact_text(text: str) -> str:
78
+ """Scrub bearer tokens and accessToken values from arbitrary text.
79
+
80
+ Pure function; no side effects. Used by `redact_error` and available
81
+ for use on any raw response body / stderr string before it reaches
82
+ a log or exception message.
83
+
84
+ renamed from `_redact_text` to a public symbol so
85
+ cross-module callers (sf_cli._redact_subprocess_stderr) import a stable
86
+ name instead of reaching into a module-private. The `_redact_text`
87
+ alias below is retained for backwards compatibility with any caller
88
+ that still imports the underscore-prefixed form; it will be removed
89
+ in a future batch once all callers migrate.
90
+ """
91
+ if not text:
92
+ return text
93
+ text = _AUTH_HEADER_RE.sub(r"\1<redacted>", text)
94
+ text = _ACCESS_TOKEN_QS_RE.sub(r"\1<redacted>", text)
95
+ text = _ACCESS_TOKEN_JSON_RE.sub(r'\1<redacted>', text)
96
+ return text
97
+
98
+
99
+ # deprecated alias. Retained so existing tests and any lingering
100
+ # `from rest_client import _redact_text` imports keep working. New code
101
+ # MUST use `redact_text` (public). Planned removal in a follow-up batch
102
+ # once the codebase is clean.
103
+ _redact_text = redact_text
104
+
105
+
106
+ def redact_error(exc: BaseException) -> str:
107
+ """Return a safe string representation of `exc`.
108
+
109
+ guaranteed to:
110
+ * include the exception type name (for triage)
111
+ * include the exception message WITH bearer tokens scrubbed
112
+ * NEVER include raw header collections, even if the exception carries them
113
+
114
+ For HTTPError specifically, we do NOT call `exc.read()` — the caller is
115
+ responsible for reading the body once (reading twice is usually a no-op
116
+ but we avoid the extra I/O and any token embedded in the body is scrubbed
117
+ at the caller via `redact_text` when it surfaces in the error message).
118
+ """
119
+ exc_type = type(exc).__name__
120
+ try:
121
+ raw = str(exc)
122
+ except Exception:
123
+ raw = "<unreprable>"
124
+ return f"{exc_type}: {redact_text(raw)}"
125
+
126
+
127
+ # -----------------------------------------------------------------------------
128
+ # cross-host redirect strips Authorization
129
+ # -----------------------------------------------------------------------------
130
+
131
+
132
+ class StripAuthOnCrossHostRedirect(urllib.request.HTTPRedirectHandler):
133
+ """Strip Authorization header on any cross-host redirect.
134
+
135
+ Python's default HTTPRedirectHandler preserves request headers across
136
+ 301/302/303/307/308. When `instanceUrl` is the trusted origin and a
137
+ redirect points at ANY other hostname, we treat the Authorization
138
+ header as tainted and drop it before the follow-up request goes out.
139
+
140
+ Hostname comparison is case-insensitive (DNS is case-insensitive).
141
+ We compare `urlparse(...).hostname` — which strips port and userinfo
142
+ — because a port change on the same host is NOT a credential-leak
143
+ vector, but treating it as cross-host would break legitimate
144
+ `:443` → bare-host redirects.
145
+ """
146
+
147
+ def redirect_request(self, req, fp, code, msg, headers, newurl):
148
+ orig_host = urllib.parse.urlparse(req.full_url).hostname
149
+ new_host = urllib.parse.urlparse(newurl).hostname
150
+ # Normalize for case-insensitive comparison. `None == None` is safe
151
+ # (both malformed URLs treated as "same host" — urllib would fail
152
+ # the follow-up anyway).
153
+ same_host = (orig_host or "").lower() == (new_host or "").lower()
154
+
155
+ if same_host:
156
+ # Default behavior: preserve Authorization (same-host redirect is
157
+ # standard practice; stripping would break legitimate OAuth flows).
158
+ return super().redirect_request(req, fp, code, msg, headers, newurl)
159
+
160
+ # cross-host redirect — build a NEW Request without any
161
+ # Authorization header. We cannot mutate `req` in place because
162
+ # urllib's default handler reuses it; callers that retain a reference
163
+ # would observe the mutation.
164
+ new_req = super().redirect_request(req, fp, code, msg, headers, newurl)
165
+ if new_req is None:
166
+ return None
167
+ # Authorization may have been set via add_header (lowercased internal
168
+ # storage as "Authorization") or via unredirected_hdrs. Remove both.
169
+ # Note: `Request.headers` stores header names in the form produced by
170
+ # `.capitalize()` (so "Authorization" lands as "Authorization" since
171
+ # .capitalize() leaves single-word strings unchanged at the first
172
+ # letter — but we defensively pop both casings).
173
+ for hdr_name in list(new_req.headers.keys()):
174
+ if hdr_name.lower() == "authorization":
175
+ new_req.headers.pop(hdr_name, None)
176
+ for hdr_name in list(new_req.unredirected_hdrs.keys()):
177
+ if hdr_name.lower() == "authorization":
178
+ new_req.unredirected_hdrs.pop(hdr_name, None)
179
+ return new_req
180
+
181
+
182
+ def build_opener() -> urllib.request.OpenerDirector:
183
+ """Return an OpenerDirector wired with StripAuthOnCrossHostRedirect.
184
+
185
+ every REST call in this module must go through an opener built
186
+ here. Direct use of `urllib.request.urlopen(...)` bypasses the redirect
187
+ handler and reintroduces the cross-host-token-leak vulnerability.
188
+ """
189
+ return urllib.request.build_opener(StripAuthOnCrossHostRedirect())
190
+
191
+
192
+ # -----------------------------------------------------------------------------
193
+ # 401 token refresh decorator + tooling/data query helpers
194
+ # -----------------------------------------------------------------------------
195
+ # Motivation: `sf org display` returns an access token with a short TTL
196
+ # (typically 15min–2h). A pipeline run with many Flow / Tooling fetches can
197
+ # exceed TTL. Without a refresh path the pipeline fails mid-run with a raw
198
+ # 401 and the user has to re-run from scratch.
199
+ #
200
+ # Design: `retry_on_401` is a decorator-factory that takes a `refresh_fn`
201
+ # closure. On 401 (HTTP status OR body contains INVALID_SESSION_ID) it
202
+ # invokes `refresh_fn()`, retries the wrapped call ONCE, and surfaces the
203
+ # original error if the retry also 401s. The refresh closure is passed
204
+ # per-call rather than stored globally so the client stays stateless and
205
+ # testable — each caller wires its own `lambda: run_sf("org_display", ...)`.
206
+ #
207
+ # Redaction: any error text that surfaces from this decorator runs through
208
+ # `redact_error` / `redact_text` . The 401 body is read once and
209
+ # scrubbed before being used for INVALID_SESSION_ID detection — we never
210
+ # log or re-raise raw bytes from the wire.
211
+
212
+ _INVALID_SESSION_MARKER = "INVALID_SESSION_ID"
213
+
214
+
215
+ _ERROR_ENVELOPE_KEYS = ("errors", "error", "errorDetails", "messages")
216
+
217
+
218
+ def _body_indicates_invalid_session(body: Any) -> bool:
219
+ """detect `INVALID_SESSION_ID` in a parsed or raw response body.
220
+
221
+ Some SF endpoints (notably a few Tooling paths) return HTTP 200 with an
222
+ error body rather than 401. The documented SF error shape is a list of
223
+ `{"errorCode": "INVALID_SESSION_ID", "message": "..."}` dicts at the
224
+ body root.
225
+
226
+ BUGFIX (2026-05-03): the prior implementation recursed into ALL
227
+ `dict.values()` and fell back to substring-matching the stringified
228
+ form. Tooling Query responses for ApexClass include a `records` list
229
+ whose `Body` field is the raw Apex source — which can legitimately
230
+ contain the literal string `INVALID_SESSION_ID` (as an errorCode
231
+ constant referenced by catch/rethrow code, e.g. `XCSF_FlowFaultMessage`
232
+ and `SkillRulesMatchAction` in the real-org fixture). That tripped the
233
+ value-walk and the substring fallback, misclassifying a 200 OK success
234
+ response as an auth failure and forcing a spurious
235
+ `INVALID_SESSION_ID after refresh` envelope into Wave B's `_unresolved`.
236
+
237
+ The tightened rules:
238
+ 1. Top-level list/tuple: the SF error envelope — recurse (one level
239
+ is enough in practice, but recursion is safe because list items
240
+ are error dicts, not data rows with Apex bodies).
241
+ 2. Dict: a positive match requires `errorCode == INVALID_SESSION_ID`
242
+ at THIS level, OR recursion into a well-known error-envelope key
243
+ (`errors`, `error`, `errorDetails`, `messages`). Data keys like
244
+ `records`, `Body`, `attributes` are NEVER walked — that was the
245
+ bug. Typos / shape variants still surface through the
246
+ error-envelope keys, which is the real world observed-shape set.
247
+ 3. Top-level raw str/bytes: an unparsed error body. A substring
248
+ match here is still safe because this helper is only called on
249
+ the TOP-LEVEL parsed response (never on a field inside a success
250
+ dict), so a raw-string body means SF literally returned text
251
+ rather than JSON — in which case substring match is the best
252
+ we can do.
253
+ 4. Anything else (int, float, None, other types): not an error
254
+ shape. Return False.
255
+ """
256
+ if body is None:
257
+ return False
258
+ if isinstance(body, (list, tuple)):
259
+ return any(_body_indicates_invalid_session(item) for item in body)
260
+ if isinstance(body, dict):
261
+ code = body.get("errorCode") or body.get("error_code")
262
+ if isinstance(code, str) and _INVALID_SESSION_MARKER in code:
263
+ return True
264
+ # Only recurse into known error-envelope keys. Walking arbitrary
265
+ # values misclassifies legitimate success payloads whose data
266
+ # fields (e.g. ApexClass.Body) happen to contain the string
267
+ # `INVALID_SESSION_ID`.
268
+ for key in _ERROR_ENVELOPE_KEYS:
269
+ if key in body and _body_indicates_invalid_session(body[key]):
270
+ return True
271
+ return False
272
+ if isinstance(body, (str, bytes)):
273
+ text = body.decode("utf-8", errors="replace") if isinstance(body, bytes) else body
274
+ return _INVALID_SESSION_MARKER in text
275
+ return False
276
+
277
+
278
+ class _InvalidSessionSignal(Exception):
279
+ """Internal-only: tunnel INVALID_SESSION_ID-in-200-body through retry_on_401.
280
+
281
+ Never escapes this module — the decorator catches it, refreshes, retries,
282
+ and on a second failure surfaces a RestClientError (redacted).
283
+ """
284
+
285
+
286
+ def _is_invalid_session_403(exc: urllib.error.HTTPError) -> bool:
287
+ """detect HTTP 403 + `INVALID_SESSION_ID` in body.
288
+
289
+ Some SF endpoints respond to an expired session with `403 Forbidden`
290
+ (body: `{"errorCode": "INVALID_SESSION_ID"}`) instead of 401. We treat
291
+ that shape as auth-refresh-triggering just like 401. Any OTHER 403
292
+ (permission denied, IP restriction, etc.) propagates unchanged — we
293
+ don't burn a refresh on a non-auth 403.
294
+
295
+ Reading `.read()` on an HTTPError is a one-shot — callers that surface
296
+ this exception must not re-read the body. For our retry path this is
297
+ fine: the decorator consumes the body once to decide, then discards
298
+ the exception after retry.
299
+ """
300
+ if exc.code != 403:
301
+ return False
302
+ try:
303
+ body = exc.read()
304
+ except Exception:
305
+ return False
306
+ if not body:
307
+ return False
308
+ try:
309
+ text = body.decode("utf-8", errors="replace") if isinstance(body, bytes) else str(body)
310
+ except Exception:
311
+ return False
312
+ return _INVALID_SESSION_MARKER in text
313
+
314
+
315
+ def retry_on_401(
316
+ refresh_fn: Callable[[], Tuple[str, str]],
317
+ ) -> Callable[[Callable[..., Any]], Callable[..., Any]]:
318
+ """Decorator: retry a REST call ONCE after refreshing the session token.
319
+
320
+ detects three auth-failure shapes:
321
+ 1. `urllib.error.HTTPError` with `code == 401`
322
+ 2. `urllib.error.HTTPError` with `code == 403` AND body contains
323
+ `INVALID_SESSION_ID` (some SF endpoints return 403 for
324
+ expired sessions; non-auth 403 is NOT treated as refreshable).
325
+ 3. HTTP 200 with response body containing `INVALID_SESSION_ID` —
326
+ callers signal this by raising `_InvalidSessionSignal`; the
327
+ helpers below do the body inspection before returning.
328
+
329
+ (REMEDIATE): the retry contract is documented and enforced here:
330
+ `refresh_fn()` returns `(instance_url, access_token)` and the caller
331
+ MUST thread those fresh credentials into the NEXT invocation of the
332
+ wrapped callable. See `tooling_query` / `data_query`, which implement
333
+ this via the `creds_provider` closure pattern (Option A). The previous
334
+ design captured credentials in a closure at call time — refresh
335
+ returned fresh creds, the wrapped closure kept using stale ones, and
336
+ the retry 401'd again. That's the defect. `retry_on_401` itself
337
+ is still credential-agnostic; the invariant lives in the caller.
338
+
339
+ If the retry ALSO fails (401 / 403+INVALID_SESSION_ID / INVALID_SESSION_ID),
340
+ the ORIGINAL error is re-raised (wrapped as `RestClientError` with a
341
+ redacted message) — not the retry's error. Rationale: the user sees
342
+ the first-attempt context (what call failed) rather than a downstream
343
+ artefact of the retry path.
344
+
345
+ Non-auth errors (500, 403 without INVALID_SESSION_ID, network timeout,
346
+ etc.) propagate unchanged — refresh is NOT attempted.
347
+ """
348
+
349
+ def _is_auth_http_error(exc: urllib.error.HTTPError) -> bool:
350
+ # 401 is the classic shape; 403+INVALID_SESSION_ID is a
351
+ # variant observed on some SF endpoints. All other 4xx/5xx
352
+ # propagate without refresh.
353
+ if exc.code == 401:
354
+ return True
355
+ if _is_invalid_session_403(exc):
356
+ return True
357
+ return False
358
+
359
+ def decorator(fn: Callable[..., Any]) -> Callable[..., Any]:
360
+ @functools.wraps(fn)
361
+ def wrapper(*args: Any, **kwargs: Any) -> Any:
362
+ try:
363
+ return fn(*args, **kwargs)
364
+ except urllib.error.HTTPError as first_exc:
365
+ if not _is_auth_http_error(first_exc):
366
+ raise
367
+ # auth-failure path — refresh and retry once.
368
+ try:
369
+ refresh_fn()
370
+ except Exception as refresh_exc:
371
+ # Refresh itself failed — surface the REFRESH error so
372
+ # the user knows they need to re-auth, but keep token
373
+ # redaction on the message.
374
+ raise RestClientError(
375
+ f"token refresh failed after auth error: {redact_error(refresh_exc)}"
376
+ ) from None
377
+ try:
378
+ return fn(*args, **kwargs)
379
+ except urllib.error.HTTPError as retry_exc:
380
+ if _is_auth_http_error(retry_exc):
381
+ raise RestClientError(
382
+ f"auth error after refresh (original): {redact_error(first_exc)}"
383
+ ) from None
384
+ raise
385
+ except _InvalidSessionSignal:
386
+ raise RestClientError(
387
+ f"INVALID_SESSION_ID after refresh (original): "
388
+ f"{redact_error(first_exc)}"
389
+ ) from None
390
+ except _InvalidSessionSignal as first_sig:
391
+ # body-path INVALID_SESSION_ID — refresh and retry once.
392
+ try:
393
+ refresh_fn()
394
+ except Exception as refresh_exc:
395
+ raise RestClientError(
396
+ f"token refresh failed after INVALID_SESSION_ID: "
397
+ f"{redact_error(refresh_exc)}"
398
+ ) from None
399
+ try:
400
+ return fn(*args, **kwargs)
401
+ except urllib.error.HTTPError as retry_exc:
402
+ if _is_auth_http_error(retry_exc):
403
+ raise RestClientError(
404
+ f"auth error after refresh (original INVALID_SESSION_ID): "
405
+ f"{redact_error(first_sig)}"
406
+ ) from None
407
+ raise
408
+ except _InvalidSessionSignal:
409
+ raise RestClientError(
410
+ f"INVALID_SESSION_ID after refresh: {redact_error(first_sig)}"
411
+ ) from None
412
+
413
+ return wrapper
414
+
415
+ return decorator
416
+
417
+
418
+ # -----------------------------------------------------------------------------
419
+ # transient HTTP (429/503) exponential backoff
420
+ # -----------------------------------------------------------------------------
421
+ # Motivation: SF REST endpoints occasionally return 429 (rate-limited) or
422
+ # 503 (service unavailable) during API-limit windows, planned maintenance,
423
+ # or transient edge hiccups. Without a retry the pipeline fails mid-run on
424
+ # what is almost always a recoverable condition. Three attempts with
425
+ # exponential backoff (1s → 2s → 4s, roughly — `base_delay * 2**attempt`)
426
+ # resolve the vast majority of transient blips without meaningfully
427
+ # extending happy-path latency.
428
+ #
429
+ # Scope (what this decorator does NOT do):
430
+ # * Not a 401 refresher — that's `retry_on_401`. The layering is:
431
+ # retry_on_401( retry_on_transient_http()( _query_once ) )
432
+ # so 429s retry first (inner), and a 401 surfacing through that layer
433
+ # triggers the outer refresh. The two concerns stay independent.
434
+ # * No jitter. Bounded to 3 attempts + short delays; adding jitter here
435
+ # would only matter if many callers burst-retried against the same
436
+ # endpoint, which is not the shape of this skill's traffic.
437
+ # * No retry on 5xx generally. Only 503 is retried because 500/502/504
438
+ # often indicate deterministic server-side failures where retrying
439
+ # won't help and we'd prefer to surface fast.
440
+ #
441
+ # Retry-After handling:
442
+ # * If the header is present and parses as a non-negative number of
443
+ # seconds OR an HTTP-date, we honor it — bounded below by base_delay *
444
+ # 2**attempt so a "Retry-After: 0" doesn't disarm the backoff.
445
+ # * Negative / malformed values fall back to the exponential schedule.
446
+ #
447
+ # Redaction: any HTTPError surfaced from the final failure goes through
448
+ # `redact_error` at the caller's message site (see `_query_once`'s wrapper
449
+ # chain). We do NOT stringify headers here — no `str(exc.headers)` or
450
+ # `resp.read()` debug dumps on retry. URL logging strips the query string.
451
+
452
+
453
+ def _parse_retry_after(raw: str | None) -> float | None:
454
+ """Parse a Retry-After header value to seconds.
455
+
456
+ Per RFC 9110 the value is either `delta-seconds` (non-negative int) or
457
+ an HTTP-date. We accept floats too — some clients emit decimal seconds
458
+ and the spec-strict int parse would silently lose them.
459
+
460
+ Returns None for missing / malformed input; caller falls back to the
461
+ exponential schedule.
462
+ """
463
+ if not raw:
464
+ return None
465
+ raw = raw.strip()
466
+ try:
467
+ secs = float(raw)
468
+ if secs < 0:
469
+ return None
470
+ return secs
471
+ except ValueError:
472
+ pass
473
+ # HTTP-date form. `parsedate_to_datetime` raises on malformed input on
474
+ # 3.10+; wrap defensively.
475
+ try:
476
+ target = email.utils.parsedate_to_datetime(raw)
477
+ except (TypeError, ValueError):
478
+ return None
479
+ if target is None:
480
+ return None
481
+ # Header expresses an absolute time; convert to a delta from now.
482
+ now = time.time()
483
+ delta = target.timestamp() - now
484
+ if delta < 0:
485
+ return 0.0
486
+ return delta
487
+
488
+
489
+ def retry_on_transient_http(
490
+ max_retries: int = 3,
491
+ base_delay: float = 1.0,
492
+ ) -> Callable[[Callable[..., Any]], Callable[..., Any]]:
493
+ """Decorator: retry on HTTP 429/503 up to `max_retries` times.
494
+
495
+ on `urllib.error.HTTPError` with code in {429, 503}, sleep and
496
+ retry. Any other exception (including 401 — let `retry_on_401`
497
+ handle that, and any 5xx besides 503) propagates immediately.
498
+
499
+ Retry semantics:
500
+ * max_retries: number of retries AFTER the first attempt (default 3).
501
+ * Total attempts = 1 + max_retries (default 4: 1 original + 3 retries).
502
+ * Final attempt's exception propagates; no further retry.
503
+
504
+ Delay per attempt = max(Retry-After-seconds, base_delay * 2**attempt).
505
+ If Retry-After is absent or malformed, falls back to the exponential
506
+ schedule. `attempt` is 0-indexed for the first retry (so the first
507
+ sleep is base_delay * 1 = base_delay).
508
+
509
+ Logs a DEBUG breadcrumb per retry via the module-level `logger`. No
510
+ header values are logged — only the HTTP code + computed delay +
511
+ attempt counter.
512
+ """
513
+
514
+ def decorator(fn: Callable[..., Any]) -> Callable[..., Any]:
515
+ @functools.wraps(fn)
516
+ def wrapper(*args: Any, **kwargs: Any) -> Any:
517
+ # Total attempts = 1 original + max_retries retries. `attempt`
518
+ # below is 0-indexed over the retry slots, which matches the
519
+ # exponential formula base_delay * 2**attempt starting at
520
+ # base_delay for attempt=0.
521
+ for attempt in range(max_retries):
522
+ try:
523
+ return fn(*args, **kwargs)
524
+ except urllib.error.HTTPError as exc:
525
+ if exc.code not in (429, 503):
526
+ # Any other HTTP status (including 401) — out of
527
+ # scope for this decorator. Propagate so outer
528
+ # layers (retry_on_401) can handle it.
529
+ raise
530
+ retry_after_raw = None
531
+ try:
532
+ # `exc.headers` is a Message-like; .get tolerates
533
+ # the case where headers are absent on some
534
+ # synthesized HTTPErrors.
535
+ retry_after_raw = exc.headers.get("Retry-After") if exc.headers else None
536
+ except Exception:
537
+ retry_after_raw = None
538
+ hinted = _parse_retry_after(retry_after_raw)
539
+ expo = base_delay * (2 ** attempt)
540
+ delay = max(hinted, expo) if hinted is not None else expo
541
+ logger.debug(
542
+ "retry_on_transient_http: HTTP %d, sleeping %.2fs (attempt %d/%d)",
543
+ exc.code, delay, attempt + 1, max_retries,
544
+ )
545
+ time.sleep(delay)
546
+ continue
547
+ # Final attempt — let any exception propagate directly. If
548
+ # this attempt raises 429/503 the caller gets the HTTPError
549
+ # unchanged (redaction happens at the wrapping call site).
550
+ return fn(*args, **kwargs)
551
+
552
+ return wrapper
553
+
554
+ return decorator
555
+
556
+
557
+ def _query_once(
558
+ instance_url: str,
559
+ token: str,
560
+ path: str,
561
+ soql: str,
562
+ ) -> dict:
563
+ """Single SOQL GET — reads body, checks for INVALID_SESSION_ID.
564
+
565
+ the Salesforce REST Query + Tooling Query
566
+ endpoints are GET-only with the SOQL passed as a urlencoded `q=`
567
+ querystring. The prior POST-with-JSON-body shape returned HTTP 405
568
+ ("Method Not Allowed") on every real-org run — confirmed by curl
569
+ against a real org:
570
+
571
+ GET /services/data/v60.0/query/?q=<soql> -> 200 OK
572
+ POST /services/data/v60.0/query/ -> 405 Method Not Allowed
573
+
574
+ We urlencode the SOQL (via `urllib.parse.urlencode({"q": soql})`)
575
+ rather than string-concatenating so spaces, single quotes, commas,
576
+ and parentheses in the query — all legal inside SOQL string
577
+ literals — travel through the wire correctly. The JSON body and
578
+ `Content-Type: application/json` header are dropped (GET carries
579
+ no body; Content-Type with a bodyless GET is technically legal but
580
+ meaningless and some edge proxies get irritated by it).
581
+
582
+ uses `build_opener()` so cross-host redirects strip Authorization.
583
+ any exception is surfaced with `redact_error`; raw body is run
584
+ through `redact_text` before it reaches an exception message.
585
+ signals `_InvalidSessionSignal` on 200-body-error so
586
+ `retry_on_401` can refresh + retry.
587
+ sets `Accept-Encoding: identity` to opt out of gzip. The
588
+ responses here are small JSON payloads — gzip would add handling
589
+ complexity (Content-Encoding detection + decompression) for zero
590
+ measurable benefit. Explicit `identity` keeps the body-inspection
591
+ path (INVALID_SESSION_ID detection) straightforward and testable.
592
+ """
593
+ # urlencode handles all SOQL-legal characters — spaces,
594
+ # single quotes inside string literals, commas, parentheses. Hand-
595
+ # rolled concatenation would have to escape each class separately.
596
+ qs = urllib.parse.urlencode({"q": soql})
597
+ url = f"{instance_url.rstrip('/')}{path}?{qs}"
598
+ req = urllib.request.Request(url, method="GET")
599
+ req.add_header("Authorization", f"Bearer {token}")
600
+ req.add_header("Accept", "application/json")
601
+ # force identity encoding — no gzip.
602
+ req.add_header("Accept-Encoding", "identity")
603
+
604
+ opener = build_opener()
605
+ try:
606
+ with opener.open(req) as resp:
607
+ raw = resp.read()
608
+ except urllib.error.HTTPError as exc:
609
+ # Bug D.1 fix: HTTPError.read() is one-shot. The default
610
+ # `redact_error()` path stringifies just `HTTP Error 4xx: <reason>`
611
+ # which loses the Salesforce error body — and that body is what
612
+ # tells the operator WHY (e.g. INVALID_FIELD, MALFORMED_QUERY,
613
+ # name-too-long, or a specific bad identifier). Pull the body
614
+ # ONCE here, scrub it via redact_text, attach it to the exception
615
+ # as `_response_body_preview`, and re-raise. Downstream callers
616
+ # that surface `redact_error(exc)` keep their existing string
617
+ # output; callers that want the body read it from the attribute.
618
+ try:
619
+ body = exc.read()
620
+ except Exception:
621
+ body = b""
622
+ try:
623
+ body_text = body.decode("utf-8", errors="replace")
624
+ except Exception:
625
+ body_text = ""
626
+ # Cap at 500 chars — enough to carry a Salesforce error array
627
+ # element with the offending name in it; small enough that
628
+ # downstream contexts don't bloat. redact_text strips bearer
629
+ # tokens defensively (body shouldn't contain one but cheaper to
630
+ # always scrub than reason about it).
631
+ exc._response_body_preview = redact_text(body_text)[:500] # type: ignore[attr-defined]
632
+ raise
633
+
634
+ try:
635
+ parsed = json.loads(raw.decode("utf-8"))
636
+ except (UnicodeDecodeError, json.JSONDecodeError) as e:
637
+ raise RestClientError(
638
+ f"malformed query response: {redact_error(e)}"
639
+ ) from None
640
+
641
+ # HTTP 200 + INVALID_SESSION_ID in body → tunnel up to the
642
+ # decorator so it can refresh + retry.
643
+ if _body_indicates_invalid_session(parsed):
644
+ raise _InvalidSessionSignal(
645
+ redact_text(json.dumps(parsed)[:500])
646
+ )
647
+ return parsed
648
+
649
+
650
+ # (REMEDIATE): credentials flow through a provider closure, not
651
+ # through function arguments that are captured once at decoration time.
652
+ #
653
+ # The previous design had a fatal defect:
654
+ #
655
+ # def tooling_query(instance_url, token, soql, *, on_401_refresh):
656
+ # @retry_on_401(on_401_refresh)
657
+ # def _call():
658
+ # return _query_once(instance_url, token, ...) # stale closure
659
+ # return _call()
660
+ #
661
+ # `refresh_fn()` returned `(new_url, new_token)` but those values were
662
+ # thrown away — the decorator only called `refresh_fn()` for its side
663
+ # effects, then re-invoked the same closure holding the ORIGINAL stale
664
+ # `instance_url` / `token`. Retry 401'd again. Feature was dead.
665
+ #
666
+ # Option A (taken): `tooling_query` / `data_query` accept a
667
+ # `creds_provider: Callable[[], Tuple[str, str]]` that is called EACH
668
+ # attempt. `refresh_fn` is responsible for mutating whatever state the
669
+ # provider reads from — typically a simple closure over a list. This
670
+ # keeps `retry_on_401` credential-agnostic and makes the contract
671
+ # ("refresh updates the source that `creds_provider` reads from")
672
+ # explicit at the caller site instead of implicit in the helper.
673
+ #
674
+ # Test: `RetryOn401CredentialRefreshIntegrationTests` in
675
+ # test_rest_client.py verifies that a real refresh carries fresh
676
+ # credentials into the retry call; the prior design fails that test.
677
+
678
+
679
+ def tooling_query(
680
+ creds_provider: Callable[[], Tuple[str, str]],
681
+ soql: str,
682
+ *,
683
+ api_version: str,
684
+ on_401_refresh: Callable[[], Tuple[str, str]],
685
+ ) -> dict:
686
+ """GET a SOQL query against the Tooling API.
687
+
688
+ `creds_provider()` is invoked on EACH attempt. `on_401_refresh`
689
+ is responsible for updating whatever state the provider reads from
690
+ before it returns — otherwise the retry would hit the same stale
691
+ credentials and 401 again.
692
+
693
+ `api_version` is a REQUIRED keyword-only arg.
694
+ The prior hardcoded `v60.0` was the source of — real orgs
695
+ run on v66 and expose fields v60 does not (confirmed empirically:
696
+ `BotDefinition.Description` exists on v66, `INVALID_FIELD` on v60).
697
+ Callers thread the version through from `main._derive_org_ids`, which
698
+ reads it once from `sf org display --json`. Making the param REQUIRED
699
+ (no default) surfaces a missed call-site as a TypeError at call time
700
+ rather than a silent regression back to a stale pinned version.
701
+ Shape is already enforced by `fs_guard.validate_api_version`
702
+ (`^v[0-9]+\\.[0-9]+$`) at the caller.
703
+
704
+ Stateless: both closures are passed per-call. Each caller owns the
705
+ storage the refresh writes to (typically a small `list[tuple[str, str]]`
706
+ cell).
707
+ """
708
+ path = f"/services/data/{api_version}/tooling/query/"
709
+
710
+ # retry_on_transient_http is the INNER layer — 429/503 retries
711
+ # happen first, and any 401 bubbling through triggers retry_on_401's
712
+ # refresh path at the outer layer. Ordering matters: if transient
713
+ # retry were outer, a 429 during a refresh retry would be mis-handled.
714
+ @retry_on_401(on_401_refresh)
715
+ @retry_on_transient_http()
716
+ def _call() -> dict:
717
+ # re-read creds on every attempt so refresh actually lands.
718
+ instance_url, token = creds_provider()
719
+ return _query_once(instance_url, token, path, soql)
720
+
721
+ return _call()
722
+
723
+
724
+ def data_query(
725
+ creds_provider: Callable[[], Tuple[str, str]],
726
+ soql: str,
727
+ *,
728
+ api_version: str,
729
+ on_401_refresh: Callable[[], Tuple[str, str]],
730
+ ) -> dict:
731
+ """GET a SOQL query against the Data API (non-Tooling).
732
+
733
+ identical retry wiring to `tooling_query`, different URL path.
734
+ `creds_provider` is invoked on every attempt so a refresh actually
735
+ propagates fresh credentials into the retry.
736
+
737
+ `api_version` is a REQUIRED keyword-only arg;
738
+ see `tooling_query` for the rationale.
739
+ """
740
+ path = f"/services/data/{api_version}/query/"
741
+
742
+ # see `tooling_query` for the decorator-ordering rationale.
743
+ @retry_on_401(on_401_refresh)
744
+ @retry_on_transient_http()
745
+ def _call() -> dict:
746
+ # re-read creds on every attempt so refresh actually lands.
747
+ instance_url, token = creds_provider()
748
+ return _query_once(instance_url, token, path, soql)
749
+
750
+ return _call()
751
+
752
+
753
+ def static_creds(instance_url: str, token: str) -> Callable[[], Tuple[str, str]]:
754
+ """Build a zero-state creds_provider that always returns the same pair.
755
+
756
+ Convenience for callers that don't wire a refresh path — tests,
757
+ single-shot scripts where a 401 is terminal. If the call 401s, the
758
+ retry sees the same creds and fails again; this is correct behavior
759
+ for an unrefreshable context (no point claiming otherwise).
760
+ """
761
+ def _provider() -> Tuple[str, str]:
762
+ return instance_url, token
763
+ return _provider