rosett-ai 1.3.3

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 (527) hide show
  1. checksums.yaml +7 -0
  2. data/.ai-provenance.yml +119 -0
  3. data/.debride_whitelist +186 -0
  4. data/.fasterer.yml +29 -0
  5. data/.mdl_style.rb +10 -0
  6. data/.mdlrc +3 -0
  7. data/.mutant.yml +49 -0
  8. data/.namespace-allowlist +42 -0
  9. data/.reek.yml +1040 -0
  10. data/.rosett-ai/config.yml +3 -0
  11. data/.rspec +5 -0
  12. data/.rubocop.yml +380 -0
  13. data/.ruby-version +1 -0
  14. data/.yamllint +51 -0
  15. data/.yardopts +12 -0
  16. data/AI-DISCLOSURE.md +48 -0
  17. data/CHANGELOG.md +519 -0
  18. data/CLAUDE.md +141 -0
  19. data/CONTRIBUTING.md +734 -0
  20. data/INSTALL.md +154 -0
  21. data/LICENSE +674 -0
  22. data/LICENSE.md +675 -0
  23. data/QUICKSTART.md +73 -0
  24. data/README.md +366 -0
  25. data/Rakefile +200 -0
  26. data/SECURITY.md +114 -0
  27. data/bin/rai +1 -0
  28. data/cliff.toml +52 -0
  29. data/conf/adopt_redactions.yml +8 -0
  30. data/conf/behaviour/.gitkeep +0 -0
  31. data/conf/compliance/cra_rules.yml +25 -0
  32. data/conf/compliance/license_rules.yml +20 -0
  33. data/conf/design/aaif_alignment.yml +181 -0
  34. data/conf/design/ab_testing.yml +172 -0
  35. data/conf/design/accessibility.yml +84 -0
  36. data/conf/design/ai_authorship.yml +210 -0
  37. data/conf/design/ai_provenance.yml +224 -0
  38. data/conf/design/ai_tool_configuration.yml +207 -0
  39. data/conf/design/architecture.yml +139 -0
  40. data/conf/design/autocompletion.yml +115 -0
  41. data/conf/design/backward_compatibility.yml +112 -0
  42. data/conf/design/behaviour_composition.yml +246 -0
  43. data/conf/design/build_rake_extraction.yml +57 -0
  44. data/conf/design/ci_pipeline.yml +100 -0
  45. data/conf/design/claude_code_configuration.yml +157 -0
  46. data/conf/design/compiler.yml +128 -0
  47. data/conf/design/comply.yml +153 -0
  48. data/conf/design/content_packs.yml +84 -0
  49. data/conf/design/desktop_integration.yml +289 -0
  50. data/conf/design/distribution.yml +216 -0
  51. data/conf/design/doctor.yml +184 -0
  52. data/conf/design/documentation.yml +152 -0
  53. data/conf/design/engine_architecture.yml +257 -0
  54. data/conf/design/error_handling.yml +103 -0
  55. data/conf/design/feature_flags.yml +142 -0
  56. data/conf/design/git_hooks.yml +165 -0
  57. data/conf/design/gui_plugins.yml +475 -0
  58. data/conf/design/i18n.yml +84 -0
  59. data/conf/design/integration_testing.yml +56 -0
  60. data/conf/design/licensing_system.yml +88 -0
  61. data/conf/design/lifecycle_management.yml +208 -0
  62. data/conf/design/mcp_integration.yml +207 -0
  63. data/conf/design/mcp_settings.yml +126 -0
  64. data/conf/design/migration.yml +56 -0
  65. data/conf/design/monitoring_observability.yml +194 -0
  66. data/conf/design/namespace_cleanup.yml +145 -0
  67. data/conf/design/plugin_test_segregation.yml +145 -0
  68. data/conf/design/policy_management.yml +229 -0
  69. data/conf/design/project_management.yml +183 -0
  70. data/conf/design/rai_mcp_asset_discovery.yml +164 -0
  71. data/conf/design/rai_mcp_server.yml +605 -0
  72. data/conf/design/release_management.yml +117 -0
  73. data/conf/design/retrofit.yml +199 -0
  74. data/conf/design/retrospective_analyzer.yml +79 -0
  75. data/conf/design/scope_hierarchy.yml +352 -0
  76. data/conf/design/security.yml +115 -0
  77. data/conf/design/session_retrospective.yml +85 -0
  78. data/conf/design/smart_ui_feedback.yml +89 -0
  79. data/conf/design/structured_logging.yml +148 -0
  80. data/conf/design/styles.yml +123 -0
  81. data/conf/design/test_peer_review.yml +89 -0
  82. data/conf/design/testing.yml +136 -0
  83. data/conf/design/threat_model.yml +108 -0
  84. data/conf/design/ui_framework.yml +111 -0
  85. data/conf/design/usage_optimization.yml +122 -0
  86. data/conf/design/version_management.yml +60 -0
  87. data/conf/design/workflow.yml +227 -0
  88. data/conf/mcp/server_defaults.yml +42 -0
  89. data/conf/mcp/trust.yml +21 -0
  90. data/conf/packaging/core.yml +12 -0
  91. data/conf/packaging/gtk4.yml +11 -0
  92. data/conf/packaging/qt6.yml +11 -0
  93. data/conf/policy/default_deny_list.yml +197 -0
  94. data/conf/review/cli-command-audit.yml +857 -0
  95. data/conf/review/design-docs.yml +1064 -0
  96. data/conf/review/design-questionnaire.yml +153 -0
  97. data/conf/review/questionnaire.yml +146 -0
  98. data/conf/review/rosett-ai-core.yml +2919 -0
  99. data/conf/schemas/ai_config_schema.json +73 -0
  100. data/conf/schemas/behaviour_schema.json +132 -0
  101. data/conf/schemas/compliance_rule_schema.json +63 -0
  102. data/conf/schemas/content_pack_manifest_schema.json +51 -0
  103. data/conf/schemas/design_schema.json +210 -0
  104. data/conf/schemas/engine_manifest_schema.json +144 -0
  105. data/conf/schemas/lockfile_schema.json +74 -0
  106. data/conf/schemas/mcp_server_schema.json +48 -0
  107. data/conf/schemas/packaging_schema.json +70 -0
  108. data/conf/schemas/policy_schema.json +85 -0
  109. data/conf/schemas/provenance_schema.json +84 -0
  110. data/conf/schemas/rai_config_schema.json +56 -0
  111. data/conf/schemas/rai_project_schema.json +20 -0
  112. data/conf/schemas/scope_hierarchy_schema.json +49 -0
  113. data/conf/schemas/target_schema.json +67 -0
  114. data/conf/schemas/tooling_schema.json +65 -0
  115. data/conf/schemas/workflow_schema.json +112 -0
  116. data/conf/targets/agents_md.yml +17 -0
  117. data/conf/targets/claude.yml +12 -0
  118. data/conf/tooling/tools.yml +58 -0
  119. data/dist/rosett-ai-mcp.service +48 -0
  120. data/dist/rosett-ai-mcp.yml.default +45 -0
  121. data/doc/AAIF_POSITIONING.md +58 -0
  122. data/doc/ADOPT.md +224 -0
  123. data/doc/AI_PROVENANCE.md +139 -0
  124. data/doc/ARCHITECTURE.md +920 -0
  125. data/doc/BEHAVIOUR.md +409 -0
  126. data/doc/BUILD.md +138 -0
  127. data/doc/CI_CD_RECIPES.md +171 -0
  128. data/doc/CLAUDE_SESSIONS_MOVED.md +16 -0
  129. data/doc/COMMAND_ANALYSIS.md +229 -0
  130. data/doc/CONFIGURATION.md +281 -0
  131. data/doc/DESIGN_AUDIT.md +235 -0
  132. data/doc/DESIGN_PEER_REVIEW.md +771 -0
  133. data/doc/DESKTOP.md +447 -0
  134. data/doc/ENGINES.md +567 -0
  135. data/doc/ENGINE_DEVELOPMENT_GUIDE.md +417 -0
  136. data/doc/FEATURE_AUDIT.md +218 -0
  137. data/doc/IMPLEMENTATION_PLAN.md +669 -0
  138. data/doc/INCIDENT_REPORT_2026-02-02.md +251 -0
  139. data/doc/MIGRATION_GUIDE.md +88 -0
  140. data/doc/PACKAGING.md +232 -0
  141. data/doc/PROJECT_DASHBOARD.md +153 -0
  142. data/doc/PULP_DEPLOYMENT.md +164 -0
  143. data/doc/QUALITY_FIX_SUMMARY.md +110 -0
  144. data/doc/QUICK_START.md +162 -0
  145. data/doc/REEK_CONFIGURATION.md +166 -0
  146. data/doc/REFERENCE.md +253 -0
  147. data/doc/REFERENCES.md +324 -0
  148. data/doc/SECURITY_REVIEW_CHECKLIST.md +72 -0
  149. data/doc/SESSION_2026-02-28_GTK4_HARDENING.md +359 -0
  150. data/doc/SETUP.md +202 -0
  151. data/doc/TEST_PEER_REVIEW.md +152 -0
  152. data/doc/THREAT_MODEL.md +230 -0
  153. data/doc/USAGE.md +545 -0
  154. data/doc/USER_MANUAL.md +585 -0
  155. data/doc/ai_test_review_checklist.md +110 -0
  156. data/doc/changes/2026-02-18-packaging-fpm.md +155 -0
  157. data/doc/changes/2026-02-19-testing-infrastructure.md +221 -0
  158. data/doc/changes/2026-02-20-security-implementation.md +281 -0
  159. data/doc/changes/2026-02-20-styles-implementation.md +220 -0
  160. data/doc/changes/2026-02-21-architecture-completion.md +95 -0
  161. data/doc/changes/2026-02-21-architecture-ui-layer.md +253 -0
  162. data/doc/changes/2026-02-21-cc-config-implementation.md +108 -0
  163. data/doc/changes/2026-02-21-ci-pipeline-implementation.md +214 -0
  164. data/doc/changes/2026-02-21-compiler-multi-target-pipeline.md +241 -0
  165. data/doc/changes/2026-02-21-config-design-show-commands.md +61 -0
  166. data/doc/changes/2026-02-21-design-implementation-overview.md +455 -0
  167. data/doc/changes/2026-02-21-lifecycle-management.md +196 -0
  168. data/doc/changes/2026-02-21-path-resolver.md +128 -0
  169. data/doc/changes/2026-02-24-ci-tmpdir-mutant-fetch.md +45 -0
  170. data/doc/changes/2026-03-01-ci-bundler-strategy.md +120 -0
  171. data/doc/changes/2026-03-20-security-hardening-phase2.md +163 -0
  172. data/doc/context/SESSION-HANDOFF.md +69 -0
  173. data/doc/context/ai-engine-usage-trends-2026.md +80 -0
  174. data/doc/context/plan-pluggable-engines.md +590 -0
  175. data/doc/decisions/001-flog-deferred.md +32 -0
  176. data/doc/decisions/002-path-resolution-strategy.md +158 -0
  177. data/doc/decisions/003-ui-adapter-selection.md +193 -0
  178. data/doc/decisions/004-design-document-validation.md +179 -0
  179. data/doc/decisions/005-package-splitting-strategy.md +200 -0
  180. data/doc/decisions/006-multi-engine-architecture.md +147 -0
  181. data/doc/decisions/007-engine-agnostic-pivot.md +219 -0
  182. data/doc/decisions/008-ci-bundler-strategy.md +129 -0
  183. data/doc/decisions/009-core-only-v1-release.md +60 -0
  184. data/doc/decisions/010-engine-debian-packaging.md +66 -0
  185. data/doc/decisions/011-context-aware-cli.md +71 -0
  186. data/doc/dependency_decisions.yml +247 -0
  187. data/doc/issues/001-wrapper-missing-environment-variables.md +197 -0
  188. data/doc/issues/002-embedded-ruby-wrong-prefix.md +217 -0
  189. data/doc/issues/003-smoke-test-false-positive.md +127 -0
  190. data/doc/issues/004-market-research-design-updates.md +109 -0
  191. data/doc/issues/005-compile-scope-coexistence.md +161 -0
  192. data/doc/locales/.gitkeep +0 -0
  193. data/doc/man/rai.1.ronn +505 -0
  194. data/doc/operations/packaging.md +133 -0
  195. data/doc/operations/rosett-ai-release.md +65 -0
  196. data/doc/reference/error-catalog.md +107 -0
  197. data/doc/reference/rosett-ai-technical-reference.pdf +0 -0
  198. data/doc/reference/src/Pictures/cover.jpg +0 -0
  199. data/doc/reference/src/Pictures/head1.jpg +0 -0
  200. data/doc/reference/src/Pictures/head2.jpg +0 -0
  201. data/doc/reference/src/Pictures/head3.jpg +0 -0
  202. data/doc/reference/src/Pictures/head4.jpg +0 -0
  203. data/doc/reference/src/Pictures/head5.jpg +0 -0
  204. data/doc/reference/src/Pictures/head6.jpg +0 -0
  205. data/doc/reference/src/Pictures/head7.jpg +0 -0
  206. data/doc/reference/src/Pictures/head8.jpg +0 -0
  207. data/doc/reference/src/StyleInd.ist +4 -0
  208. data/doc/reference/src/bibliography.bib +79 -0
  209. data/doc/reference/src/main.tex +1288 -0
  210. data/doc/reference/src/structure.tex +303 -0
  211. data/doc/rosett-ai-bookmarks.html +301 -0
  212. data/kitchen.yml +46 -0
  213. data/lib/rosett_ai/adopter/executor_resolver.rb +77 -0
  214. data/lib/rosett_ai/adopter/local_analysis_collector.rb +154 -0
  215. data/lib/rosett_ai/adopter/rule_adopter.rb +254 -0
  216. data/lib/rosett_ai/ai_config/config_compiler.rb +111 -0
  217. data/lib/rosett_ai/ai_config/context_window.rb +55 -0
  218. data/lib/rosett_ai/ai_config/cost_controls.rb +44 -0
  219. data/lib/rosett_ai/ai_config/fallback_chain.rb +64 -0
  220. data/lib/rosett_ai/ai_config/model_router.rb +121 -0
  221. data/lib/rosett_ai/ai_config/validator.rb +45 -0
  222. data/lib/rosett_ai/authorship/attribution_compiler.rb +99 -0
  223. data/lib/rosett_ai/authorship/disclosure_policy.rb +81 -0
  224. data/lib/rosett_ai/authorship/review_validator.rb +39 -0
  225. data/lib/rosett_ai/authorship/trailer_generator.rb +88 -0
  226. data/lib/rosett_ai/backup/compressor.rb +180 -0
  227. data/lib/rosett_ai/backup/destination.rb +91 -0
  228. data/lib/rosett_ai/behaviour/manager.rb +156 -0
  229. data/lib/rosett_ai/compiler/backend.rb +86 -0
  230. data/lib/rosett_ai/compiler/backends/agents_md_backend.rb +80 -0
  231. data/lib/rosett_ai/compiler/backends/claude_backend.rb +88 -0
  232. data/lib/rosett_ai/compiler/backends/generic_backend.rb +15 -0
  233. data/lib/rosett_ai/compiler/behaviour_compiler.rb +40 -0
  234. data/lib/rosett_ai/compiler/capability_checker.rb +104 -0
  235. data/lib/rosett_ai/compiler/compilation_pipeline.rb +361 -0
  236. data/lib/rosett_ai/compiler/compiled_output.rb +39 -0
  237. data/lib/rosett_ai/compiler/locale_compiler.rb +250 -0
  238. data/lib/rosett_ai/compiler/target_profile.rb +112 -0
  239. data/lib/rosett_ai/completion/generator.rb +101 -0
  240. data/lib/rosett_ai/completion/shells/bash_generator.rb +126 -0
  241. data/lib/rosett_ai/completion/shells/fish_generator.rb +78 -0
  242. data/lib/rosett_ai/completion/shells/zsh_generator.rb +126 -0
  243. data/lib/rosett_ai/comply/checkers/cra_checker.rb +102 -0
  244. data/lib/rosett_ai/comply/checkers/license_checker.rb +85 -0
  245. data/lib/rosett_ai/comply/checkers/spdx_header_checker.rb +98 -0
  246. data/lib/rosett_ai/comply/reporter.rb +113 -0
  247. data/lib/rosett_ai/comply/runner.rb +50 -0
  248. data/lib/rosett_ai/composition/circular_dependency_detector.rb +56 -0
  249. data/lib/rosett_ai/composition/composer.rb +158 -0
  250. data/lib/rosett_ai/composition/composition_result.rb +64 -0
  251. data/lib/rosett_ai/composition/conflict_detector.rb +53 -0
  252. data/lib/rosett_ai/composition/lockfile.rb +103 -0
  253. data/lib/rosett_ai/composition/merge_strategy.rb +131 -0
  254. data/lib/rosett_ai/composition/priority_sorter.rb +29 -0
  255. data/lib/rosett_ai/composition/scope_resolver.rb +55 -0
  256. data/lib/rosett_ai/config/compile_result.rb +37 -0
  257. data/lib/rosett_ai/config/compiler.rb +13 -0
  258. data/lib/rosett_ai/config/domain_transformer.rb +13 -0
  259. data/lib/rosett_ai/config/key_map.rb +13 -0
  260. data/lib/rosett_ai/config/masking_secret_resolver.rb +40 -0
  261. data/lib/rosett_ai/config/scope_router.rb +13 -0
  262. data/lib/rosett_ai/config/secret_resolver.rb +125 -0
  263. data/lib/rosett_ai/configuration.rb +119 -0
  264. data/lib/rosett_ai/content/content_client.rb +60 -0
  265. data/lib/rosett_ai/content/pack_installer.rb +117 -0
  266. data/lib/rosett_ai/content/pack_manifest.rb +50 -0
  267. data/lib/rosett_ai/content/pack_registry.rb +68 -0
  268. data/lib/rosett_ai/content_packs/manager.rb +50 -0
  269. data/lib/rosett_ai/dbus/compositor_detector.rb +77 -0
  270. data/lib/rosett_ai/dbus/focus_adapters/base.rb +59 -0
  271. data/lib/rosett_ai/dbus/focus_adapters/gnome_adapter.rb +172 -0
  272. data/lib/rosett_ai/dbus/focus_adapters/hyprland_adapter.rb +77 -0
  273. data/lib/rosett_ai/dbus/focus_adapters/i3_adapter.rb +65 -0
  274. data/lib/rosett_ai/dbus/focus_adapters/kwin_adapter.rb +103 -0
  275. data/lib/rosett_ai/dbus/focus_adapters/x11_adapter.rb +105 -0
  276. data/lib/rosett_ai/dbus/focus_monitor_interface.rb +103 -0
  277. data/lib/rosett_ai/dbus/manager_interface.rb +213 -0
  278. data/lib/rosett_ai/dbus/plugin_manager_interface.rb +169 -0
  279. data/lib/rosett_ai/dbus/rate_limiter.rb +89 -0
  280. data/lib/rosett_ai/dbus/service.rb +121 -0
  281. data/lib/rosett_ai/dbus/status_notifier_interface.rb +79 -0
  282. data/lib/rosett_ai/deprecation.rb +79 -0
  283. data/lib/rosett_ai/desktop/dbus_client.rb +259 -0
  284. data/lib/rosett_ai/desktop/gtk4_app.rb +371 -0
  285. data/lib/rosett_ai/desktop/gtk4_preferences.rb +331 -0
  286. data/lib/rosett_ai/desktop/gui_logger.rb +236 -0
  287. data/lib/rosett_ai/doctor/check.rb +92 -0
  288. data/lib/rosett_ai/doctor/checks/cache_health_check.rb +50 -0
  289. data/lib/rosett_ai/doctor/checks/dbus_availability_check.rb +39 -0
  290. data/lib/rosett_ai/doctor/checks/engine_detection_check.rb +46 -0
  291. data/lib/rosett_ai/doctor/checks/file_permission_check.rb +44 -0
  292. data/lib/rosett_ai/doctor/checks/gem_dependency_check.rb +55 -0
  293. data/lib/rosett_ai/doctor/checks/ruby_version_check.rb +50 -0
  294. data/lib/rosett_ai/doctor/checks/stale_config_nncc_check.rb +57 -0
  295. data/lib/rosett_ai/doctor/checks/stale_home_nncc_check.rb +59 -0
  296. data/lib/rosett_ai/doctor.rb +81 -0
  297. data/lib/rosett_ai/documentation/reference_compiler.rb +122 -0
  298. data/lib/rosett_ai/documentation/translator.rb +62 -0
  299. data/lib/rosett_ai/engines/base_config_compiler.rb +203 -0
  300. data/lib/rosett_ai/engines/detector.rb +63 -0
  301. data/lib/rosett_ai/engines/registry.rb +50 -0
  302. data/lib/rosett_ai/error_handler.rb +139 -0
  303. data/lib/rosett_ai/exit_codes.rb +76 -0
  304. data/lib/rosett_ai/feature_flags.rb +102 -0
  305. data/lib/rosett_ai/formatting.rb +33 -0
  306. data/lib/rosett_ai/gem_consistency_checker.rb +199 -0
  307. data/lib/rosett_ai/git_hooks/chain_detector.rb +86 -0
  308. data/lib/rosett_ai/git_hooks/installer.rb +175 -0
  309. data/lib/rosett_ai/git_hooks/script_generator.rb +125 -0
  310. data/lib/rosett_ai/gitlab/validators/supplementary_gitlab_ci_yaml_validator.rb +79 -0
  311. data/lib/rosett_ai/i18n/locale_resolver.rb +46 -0
  312. data/lib/rosett_ai/i18n/utf8_checker.rb +32 -0
  313. data/lib/rosett_ai/init/config_file_writer.rb +24 -0
  314. data/lib/rosett_ai/init/directory_builder.rb +38 -0
  315. data/lib/rosett_ai/init/file_copier.rb +95 -0
  316. data/lib/rosett_ai/init/global_initializer.rb +28 -0
  317. data/lib/rosett_ai/init/local_initializer.rb +27 -0
  318. data/lib/rosett_ai/init/mcp_registrar.rb +109 -0
  319. data/lib/rosett_ai/init/project_initializer.rb +38 -0
  320. data/lib/rosett_ai/licensing/license_key.rb +139 -0
  321. data/lib/rosett_ai/licensing/license_store.rb +64 -0
  322. data/lib/rosett_ai/licensing/license_validator.rb +60 -0
  323. data/lib/rosett_ai/licensing/tier.rb +42 -0
  324. data/lib/rosett_ai/mcp/admin/auditor.rb +88 -0
  325. data/lib/rosett_ai/mcp/admin/health_checker.rb +81 -0
  326. data/lib/rosett_ai/mcp/admin/registry.rb +100 -0
  327. data/lib/rosett_ai/mcp/admin/schema_validator.rb +63 -0
  328. data/lib/rosett_ai/mcp/enforcement/.gitkeep +0 -0
  329. data/lib/rosett_ai/mcp/enforcement/hook_generator.rb +197 -0
  330. data/lib/rosett_ai/mcp/enforcement/validator.rb +215 -0
  331. data/lib/rosett_ai/mcp/governance.rb +160 -0
  332. data/lib/rosett_ai/mcp/http_security_config.rb +158 -0
  333. data/lib/rosett_ai/mcp/instructions.rb +266 -0
  334. data/lib/rosett_ai/mcp/key_hasher.rb +66 -0
  335. data/lib/rosett_ai/mcp/keyfile.rb +221 -0
  336. data/lib/rosett_ai/mcp/middleware/authentication.rb +146 -0
  337. data/lib/rosett_ai/mcp/middleware/content_type.rb +56 -0
  338. data/lib/rosett_ai/mcp/middleware/cors.rb +83 -0
  339. data/lib/rosett_ai/mcp/middleware/origin_validation.rb +73 -0
  340. data/lib/rosett_ai/mcp/middleware/rate_limit.rb +106 -0
  341. data/lib/rosett_ai/mcp/middleware/request_size.rb +51 -0
  342. data/lib/rosett_ai/mcp/plugins.rb +143 -0
  343. data/lib/rosett_ai/mcp/prompts/compilation_prompt.rb +40 -0
  344. data/lib/rosett_ai/mcp/prompts/compliance_prompt.rb +41 -0
  345. data/lib/rosett_ai/mcp/prompts/diagnostics_prompt.rb +41 -0
  346. data/lib/rosett_ai/mcp/prompts/validation_prompt.rb +41 -0
  347. data/lib/rosett_ai/mcp/resources/behaviour_resource.rb +127 -0
  348. data/lib/rosett_ai/mcp/resources/config_resource.rb +72 -0
  349. data/lib/rosett_ai/mcp/resources/design_resource.rb +58 -0
  350. data/lib/rosett_ai/mcp/resources/hooks_resource.rb +74 -0
  351. data/lib/rosett_ai/mcp/resources/provenance_resource.rb +51 -0
  352. data/lib/rosett_ai/mcp/resources/rules_resource.rb +60 -0
  353. data/lib/rosett_ai/mcp/resources/schema_resource.rb +72 -0
  354. data/lib/rosett_ai/mcp/response_helper.rb +46 -0
  355. data/lib/rosett_ai/mcp/security_logger.rb +60 -0
  356. data/lib/rosett_ai/mcp/server.rb +212 -0
  357. data/lib/rosett_ai/mcp/settings/server_installer.rb +112 -0
  358. data/lib/rosett_ai/mcp/settings/trust_manager.rb +142 -0
  359. data/lib/rosett_ai/mcp/tools/adopt_tool.rb +70 -0
  360. data/lib/rosett_ai/mcp/tools/backup_tool.rb +64 -0
  361. data/lib/rosett_ai/mcp/tools/behaviour_display_tool.rb +72 -0
  362. data/lib/rosett_ai/mcp/tools/behaviour_list_tool.rb +56 -0
  363. data/lib/rosett_ai/mcp/tools/behaviour_manage_tool.rb +114 -0
  364. data/lib/rosett_ai/mcp/tools/behaviour_show_tool.rb +62 -0
  365. data/lib/rosett_ai/mcp/tools/compile_status_tool.rb +122 -0
  366. data/lib/rosett_ai/mcp/tools/compile_tool.rb +191 -0
  367. data/lib/rosett_ai/mcp/tools/comply_tool.rb +79 -0
  368. data/lib/rosett_ai/mcp/tools/config_compile_tool.rb +71 -0
  369. data/lib/rosett_ai/mcp/tools/config_status_tool.rb +79 -0
  370. data/lib/rosett_ai/mcp/tools/content_tool.rb +78 -0
  371. data/lib/rosett_ai/mcp/tools/context_query_tool.rb +156 -0
  372. data/lib/rosett_ai/mcp/tools/design_list_tool.rb +57 -0
  373. data/lib/rosett_ai/mcp/tools/design_show_tool.rb +69 -0
  374. data/lib/rosett_ai/mcp/tools/doctor_tool.rb +62 -0
  375. data/lib/rosett_ai/mcp/tools/documentation_status_tool.rb +45 -0
  376. data/lib/rosett_ai/mcp/tools/engines_tool.rb +84 -0
  377. data/lib/rosett_ai/mcp/tools/hook_install_tool.rb +190 -0
  378. data/lib/rosett_ai/mcp/tools/hook_preview_tool.rb +173 -0
  379. data/lib/rosett_ai/mcp/tools/hooks_status_tool.rb +84 -0
  380. data/lib/rosett_ai/mcp/tools/init_tool.rb +87 -0
  381. data/lib/rosett_ai/mcp/tools/license_status_tool.rb +44 -0
  382. data/lib/rosett_ai/mcp/tools/project_tool.rb +117 -0
  383. data/lib/rosett_ai/mcp/tools/provenance_tool.rb +97 -0
  384. data/lib/rosett_ai/mcp/tools/provenance_write_tool.rb +40 -0
  385. data/lib/rosett_ai/mcp/tools/retrofit_tool.rb +81 -0
  386. data/lib/rosett_ai/mcp/tools/rule_search_tool.rb +163 -0
  387. data/lib/rosett_ai/mcp/tools/schema_get_tool.rb +94 -0
  388. data/lib/rosett_ai/mcp/tools/tooling_tool.rb +86 -0
  389. data/lib/rosett_ai/mcp/tools/validate_tool.rb +105 -0
  390. data/lib/rosett_ai/mcp/tools/workflow_execute_tool.rb +74 -0
  391. data/lib/rosett_ai/mcp/tools/workflow_tool.rb +78 -0
  392. data/lib/rosett_ai/migration/detector.rb +117 -0
  393. data/lib/rosett_ai/migration/nncc_config_migrator.rb +94 -0
  394. data/lib/rosett_ai/migration/nncc_project_migrator.rb +90 -0
  395. data/lib/rosett_ai/migration/xdg_migrator.rb +123 -0
  396. data/lib/rosett_ai/package_manager/apt.rb +108 -0
  397. data/lib/rosett_ai/package_manager/base.rb +68 -0
  398. data/lib/rosett_ai/package_manager/gem_backend.rb +90 -0
  399. data/lib/rosett_ai/packaging/variant_config.rb +92 -0
  400. data/lib/rosett_ai/path_resolver.rb +115 -0
  401. data/lib/rosett_ai/plugins/contract.rb +43 -0
  402. data/lib/rosett_ai/plugins/engine_contract.rb +60 -0
  403. data/lib/rosett_ai/plugins/gui_contract.rb +74 -0
  404. data/lib/rosett_ai/plugins/mcp_contract.rb +48 -0
  405. data/lib/rosett_ai/plugins/registry.rb +150 -0
  406. data/lib/rosett_ai/policy/auditor.rb +41 -0
  407. data/lib/rosett_ai/policy/deny_list.rb +71 -0
  408. data/lib/rosett_ai/policy/opt_out_scanner.rb +37 -0
  409. data/lib/rosett_ai/policy/policy_compiler.rb +84 -0
  410. data/lib/rosett_ai/policy/protected_files.rb +47 -0
  411. data/lib/rosett_ai/policy/tier_hierarchy.rb +48 -0
  412. data/lib/rosett_ai/policy/validator.rb +35 -0
  413. data/lib/rosett_ai/profiler.rb +79 -0
  414. data/lib/rosett_ai/project/drift_detector.rb +126 -0
  415. data/lib/rosett_ai/project/manager.rb +115 -0
  416. data/lib/rosett_ai/project/sync_manager.rb +138 -0
  417. data/lib/rosett_ai/project/template_applier.rb +105 -0
  418. data/lib/rosett_ai/project_context.rb +82 -0
  419. data/lib/rosett_ai/provenance/entry.rb +63 -0
  420. data/lib/rosett_ai/provenance/file_source.rb +32 -0
  421. data/lib/rosett_ai/provenance/source.rb +62 -0
  422. data/lib/rosett_ai/provenance/store.rb +153 -0
  423. data/lib/rosett_ai/provenance/tracker.rb +62 -0
  424. data/lib/rosett_ai/provenance/trailer_generator.rb +43 -0
  425. data/lib/rosett_ai/provenance/validator.rb +45 -0
  426. data/lib/rosett_ai/quorum/collector.rb +59 -0
  427. data/lib/rosett_ai/quorum/comparator.rb +81 -0
  428. data/lib/rosett_ai/quorum/dispatcher.rb +57 -0
  429. data/lib/rosett_ai/quorum/strategies/adopt.rb +56 -0
  430. data/lib/rosett_ai/rai_config.rb +107 -0
  431. data/lib/rosett_ai/retrofit/base_parser.rb +66 -0
  432. data/lib/rosett_ai/retrofit/engine.rb +171 -0
  433. data/lib/rosett_ai/retrofit/parsers/agents_md_parser.rb +50 -0
  434. data/lib/rosett_ai/retrofit/parsers/claude_parser.rb +69 -0
  435. data/lib/rosett_ai/retrofit/parsers/cursor_parser.rb +82 -0
  436. data/lib/rosett_ai/retrofit/round_trip_validator.rb +65 -0
  437. data/lib/rosett_ai/retrofit/scanner.rb +47 -0
  438. data/lib/rosett_ai/retrofit/secret_detector.rb +87 -0
  439. data/lib/rosett_ai/secrets_resolver.rb +71 -0
  440. data/lib/rosett_ai/smart_feedback/suggester.rb +83 -0
  441. data/lib/rosett_ai/smart_feedback/thor_middleware.rb +84 -0
  442. data/lib/rosett_ai/structured_logger.rb +110 -0
  443. data/lib/rosett_ai/telemetry/json_lines_writer.rb +50 -0
  444. data/lib/rosett_ai/telemetry/log_rotator.rb +67 -0
  445. data/lib/rosett_ai/telemetry/provider.rb +26 -0
  446. data/lib/rosett_ai/telemetry/reporter.rb +144 -0
  447. data/lib/rosett_ai/telemetry.rb +47 -0
  448. data/lib/rosett_ai/text_sanitizer.rb +62 -0
  449. data/lib/rosett_ai/thor/cli.rb +269 -0
  450. data/lib/rosett_ai/thor/tasks/adopt.rb +250 -0
  451. data/lib/rosett_ai/thor/tasks/backup.rb +420 -0
  452. data/lib/rosett_ai/thor/tasks/behaviour.rb +474 -0
  453. data/lib/rosett_ai/thor/tasks/build.rb +1162 -0
  454. data/lib/rosett_ai/thor/tasks/compile.rb +415 -0
  455. data/lib/rosett_ai/thor/tasks/completion.rb +123 -0
  456. data/lib/rosett_ai/thor/tasks/comply.rb +82 -0
  457. data/lib/rosett_ai/thor/tasks/config.rb +265 -0
  458. data/lib/rosett_ai/thor/tasks/content.rb +193 -0
  459. data/lib/rosett_ai/thor/tasks/dbus.rb +321 -0
  460. data/lib/rosett_ai/thor/tasks/design.rb +258 -0
  461. data/lib/rosett_ai/thor/tasks/desktop.rb +129 -0
  462. data/lib/rosett_ai/thor/tasks/doctor.rb +127 -0
  463. data/lib/rosett_ai/thor/tasks/documentation.rb +321 -0
  464. data/lib/rosett_ai/thor/tasks/engines.rb +167 -0
  465. data/lib/rosett_ai/thor/tasks/hooks.rb +219 -0
  466. data/lib/rosett_ai/thor/tasks/init.rb +259 -0
  467. data/lib/rosett_ai/thor/tasks/license.rb +120 -0
  468. data/lib/rosett_ai/thor/tasks/mcp.rb +535 -0
  469. data/lib/rosett_ai/thor/tasks/migrate.rb +121 -0
  470. data/lib/rosett_ai/thor/tasks/plugins.rb +157 -0
  471. data/lib/rosett_ai/thor/tasks/project.rb +260 -0
  472. data/lib/rosett_ai/thor/tasks/provenance.rb +195 -0
  473. data/lib/rosett_ai/thor/tasks/release.rb +314 -0
  474. data/lib/rosett_ai/thor/tasks/retrofit.rb +90 -0
  475. data/lib/rosett_ai/thor/tasks/tooling.rb +308 -0
  476. data/lib/rosett_ai/thor/tasks/validate.rb +108 -0
  477. data/lib/rosett_ai/thor/tasks/workflow.rb +196 -0
  478. data/lib/rosett_ai/tooling/ci_yaml_validator.rb +37 -0
  479. data/lib/rosett_ai/tooling/version_checker.rb +35 -0
  480. data/lib/rosett_ai/ui/accessible_tui.rb +61 -0
  481. data/lib/rosett_ai/ui/base.rb +46 -0
  482. data/lib/rosett_ai/ui/gtk4.rb +98 -0
  483. data/lib/rosett_ai/ui/kde.rb +40 -0
  484. data/lib/rosett_ai/ui/qt6.rb +40 -0
  485. data/lib/rosett_ai/ui/registry.rb +60 -0
  486. data/lib/rosett_ai/ui/tty_helper.rb +74 -0
  487. data/lib/rosett_ai/ui/tui.rb +59 -0
  488. data/lib/rosett_ai/validators/behaviour_validator.rb +20 -0
  489. data/lib/rosett_ai/validators/design_validator.rb +17 -0
  490. data/lib/rosett_ai/validators/schema_validator.rb +84 -0
  491. data/lib/rosett_ai/validators/tooling_validator.rb +17 -0
  492. data/lib/rosett_ai/version.rb +8 -0
  493. data/lib/rosett_ai/version_consistency_checker.rb +129 -0
  494. data/lib/rosett_ai/workflow/audit_log.rb +86 -0
  495. data/lib/rosett_ai/workflow/engine.rb +142 -0
  496. data/lib/rosett_ai/workflow/manager.rb +82 -0
  497. data/lib/rosett_ai/workflow/schema_validator.rb +71 -0
  498. data/lib/rosett_ai/workflow/step_runner.rb +61 -0
  499. data/lib/rosett_ai/workflow/steps/prompt_step.rb +62 -0
  500. data/lib/rosett_ai/workflow/steps/rai_step.rb +74 -0
  501. data/lib/rosett_ai/workflow/steps/shell_step.rb +53 -0
  502. data/lib/rosett_ai/yaml_loader.rb +78 -0
  503. data/lib/rosett_ai.rb +221 -0
  504. data/lib/rubocop/cop/rosett_ai/shell_interpolation.rb +54 -0
  505. data/lib/rubocop/cop/rosett_ai/unsafe_const_get.rb +60 -0
  506. data/lib/rubocop/cop/rosett_ai/unsafe_send.rb +50 -0
  507. data/lib/rubocop/cop/rosett_ai/unsafe_yaml_load.rb +40 -0
  508. data/lib/rubocop/rosett_ai.rb +9 -0
  509. data/lib/scripts/generated/docker_hub_tags.rb +126 -0
  510. data/locales/.gitkeep +0 -0
  511. data/locales/ar.yml +579 -0
  512. data/locales/en.yml +571 -0
  513. data/locales/fr.yml +567 -0
  514. data/packaging/build-engine-deb.sh +81 -0
  515. data/packaging/scripts/postinst +17 -0
  516. data/packaging/scripts/postrm +19 -0
  517. data/packaging/scripts/prerm +10 -0
  518. data/packaging/wrapper.sh.template +38 -0
  519. data/rosett-ai.gemspec +63 -0
  520. data/rules/.gitkeep +0 -0
  521. data/scripts/publish/pulp_upload.sh +123 -0
  522. data/settings.json +29 -0
  523. data/share/applications/be.neatnerds.rosettai.desktop +29 -0
  524. data/share/dbus-1/interfaces/be.neatnerds.rosettai.xml +103 -0
  525. data/share/dbus-1/services/be.neatnerds.rosettai.service +3 -0
  526. data/share/templates/behaviour/criticalthinking.yml +69 -0
  527. metadata +810 -0
@@ -0,0 +1,100 @@
1
+ # frozen_string_literal: true
2
+
3
+ # SPDX-License-Identifier: GPL-3.0-only
4
+ # Copyright (C) 2026 Hugo Antonio Sepulveda Manriquez / NeatNerds
5
+
6
+ module RosettAi
7
+ module Mcp
8
+ module Admin
9
+ # Registry of configured MCP servers.
10
+ #
11
+ # Manages discovery, storage, and querying of MCP server
12
+ # configurations from XDG-compliant paths.
13
+ #
14
+ # @author hugo
15
+ # @author claude
16
+ class Registry
17
+ CONFIG_DIR = '.config/rosett-ai/mcp/servers'
18
+
19
+ # @param config_dir [Pathname, nil] override config directory
20
+ def initialize(config_dir: nil)
21
+ @config_dir = config_dir || Pathname.new(Dir.home).join(CONFIG_DIR)
22
+ end
23
+
24
+ # Lists all registered MCP servers.
25
+ #
26
+ # @return [Array<Hash>] server entries with :name, :transport, :command/:url
27
+ def list
28
+ return [] unless @config_dir.directory?
29
+
30
+ @config_dir.glob('*.yml').filter_map { |path| load_server(path) }
31
+ end
32
+
33
+ # Finds a server by name.
34
+ #
35
+ # @param name [String] server name
36
+ # @return [Hash, nil] server entry or nil
37
+ def find(name)
38
+ list.find { |s| s[:name] == name }
39
+ end
40
+
41
+ # Adds a server configuration.
42
+ #
43
+ # @param config [Hash] server configuration
44
+ # @return [Pathname] path to the written file
45
+ def add(config)
46
+ FileUtils.mkdir_p(@config_dir)
47
+ name = config.fetch('name') { config.fetch(:name) }
48
+ path = @config_dir.join("#{name}.yml")
49
+
50
+ File.write(path, YAML.dump(stringify_keys(config)))
51
+ path
52
+ end
53
+
54
+ # Removes a server configuration.
55
+ #
56
+ # @param name [String] server name
57
+ # @return [Boolean] true if removed
58
+ def remove(name) # rubocop:disable Naming/PredicateMethod -- destructive action, not a predicate
59
+ path = @config_dir.join("#{name}.yml")
60
+ return false unless path.exist?
61
+
62
+ File.delete(path)
63
+ true
64
+ end
65
+
66
+ # Returns all server config file paths.
67
+ #
68
+ # @return [Array<Pathname>]
69
+ def config_paths
70
+ return [] unless @config_dir.directory?
71
+
72
+ @config_dir.glob('*.yml')
73
+ end
74
+
75
+ private
76
+
77
+ def load_server(path)
78
+ data = YAML.safe_load_file(path, permitted_classes: [Symbol])
79
+ return nil unless data.is_a?(Hash)
80
+
81
+ {
82
+ name: data['name'],
83
+ transport: data['transport'] || 'stdio',
84
+ command: data['command'],
85
+ url: data['url'],
86
+ args: data['args'],
87
+ env: data['env'],
88
+ source_file: path.to_s
89
+ }
90
+ rescue Psych::SyntaxError
91
+ nil
92
+ end
93
+
94
+ def stringify_keys(hash)
95
+ hash.transform_keys(&:to_s)
96
+ end
97
+ end
98
+ end
99
+ end
100
+ end
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ # SPDX-License-Identifier: GPL-3.0-only
4
+ # Copyright (C) 2026 Hugo Antonio Sepulveda Manriquez / NeatNerds
5
+
6
+ require 'json_schemer'
7
+
8
+ module RosettAi
9
+ module Mcp
10
+ module Admin
11
+ # Validates MCP server configurations against JSON Schema.
12
+ #
13
+ # Uses conf/schemas/mcp_server_schema.json for validation.
14
+ #
15
+ # @author hugo
16
+ # @author claude
17
+ class SchemaValidator
18
+ # @param schema_path [Pathname, nil] override schema file path
19
+ def initialize(schema_path: nil)
20
+ @schema_path = schema_path || RosettAi.root.join('conf', 'schemas', 'mcp_server_schema.json')
21
+ end
22
+
23
+ # Validates a server configuration hash.
24
+ #
25
+ # @param server [Hash] server configuration
26
+ # @return [Hash] result with :valid and :errors keys
27
+ def validate(server)
28
+ schema = load_schema
29
+ return { valid: true, errors: [] } unless schema
30
+
31
+ data = normalize(server)
32
+ errors = schema.validate(data).map { |e| e['error'] }
33
+
34
+ { valid: errors.empty?, errors: errors }
35
+ end
36
+
37
+ # Validates all servers in a registry.
38
+ #
39
+ # @param registry [Registry] server registry
40
+ # @return [Array<Hash>] validation results
41
+ def validate_all(registry)
42
+ registry.list.map do |server|
43
+ result = validate(server)
44
+ result.merge(name: server[:name])
45
+ end
46
+ end
47
+
48
+ private
49
+
50
+ def load_schema
51
+ return nil unless @schema_path.exist?
52
+
53
+ data = JSON.parse(File.read(@schema_path))
54
+ JSONSchemer.schema(data)
55
+ end
56
+
57
+ def normalize(server)
58
+ server.transform_keys(&:to_s).compact
59
+ end
60
+ end
61
+ end
62
+ end
63
+ end
File without changes
@@ -0,0 +1,197 @@
1
+ # frozen_string_literal: true
2
+
3
+ # SPDX-License-Identifier: GPL-3.0-only
4
+ # Copyright (C) 2026 Hugo Antonio Sepulveda Manriquez / NeatNerds
5
+
6
+ require 'time'
7
+
8
+ module RosettAi
9
+ module Mcp
10
+ module Enforcement
11
+ # Generates self-contained Ruby PreToolUse hook scripts from validated
12
+ # enforcement rules.
13
+ #
14
+ # Generated scripts are standalone — they require only Ruby stdlib (json)
15
+ # and have zero runtime dependency on rosett-ai. Each script includes:
16
+ # - YARD documentation
17
+ # - Human-readable header with generation metadata
18
+ # - AI client instructions comment block
19
+ # - Traceability (source behaviour, rule ID, version, generator version)
20
+ # - Enforcement logic: regex match on tool input, block/warn/log action
21
+ #
22
+ # @example Generate a hook script for validated rules
23
+ # generator = HookGenerator.new
24
+ # script = generator.generate(validated_rules, behaviour_name: 'session_optimization',
25
+ # behaviour_version: '1.0.0')
26
+ #
27
+ # @author hugo
28
+ # @author claude
29
+ class HookGenerator
30
+ # Generates a complete PreToolUse hook script from validated rules.
31
+ #
32
+ # @param rules [Array<Hash>] validated rules from {Validator}, each with
33
+ # :rule_id, :behaviour, :pattern, :applies_to, :action, :message,
34
+ # :priority, :description
35
+ # @param behaviour_name [String] source behaviour name
36
+ # @param behaviour_version [String] source behaviour version
37
+ # @return [String] complete Ruby hook script
38
+ def generate(rules, behaviour_name:, behaviour_version:)
39
+ timestamp = Time.now.utc.iso8601
40
+ parts = []
41
+ parts << shebang_and_magic
42
+ parts << yard_documentation(rules)
43
+ parts << traceability_header(behaviour_name, behaviour_version, rules, timestamp)
44
+ parts << ai_client_instructions
45
+ parts << rule_constant(rules)
46
+ parts << enforcement_body
47
+ parts.join("\n")
48
+ end
49
+
50
+ private
51
+
52
+ # @return [String]
53
+ def shebang_and_magic
54
+ <<~RUBY
55
+ #!/usr/bin/env ruby
56
+ # frozen_string_literal: true
57
+ RUBY
58
+ end
59
+
60
+ # Generates YARD documentation block for the hook script.
61
+ #
62
+ # @param rules [Array<Hash>]
63
+ # @return [String]
64
+ def yard_documentation(rules)
65
+ rule_list = rules.map { |r| "# - #{r[:behaviour]}/#{r[:rule_id]} (P#{r[:priority]})" }
66
+ <<~RUBY
67
+ # @title Rosett-AI Enforcement Hook
68
+ # @description Auto-generated PreToolUse hook that enforces behaviour rules
69
+ # by matching tool input against regex patterns. Self-contained — no
70
+ # external gem dependencies.
71
+ #
72
+ # @note This file is auto-generated by rosett-ai. Do not edit manually.
73
+ # Re-generate with: raictl mcp tool rai_hook_install
74
+ #
75
+ # Enforced rules:
76
+ #{rule_list.join("\n")}
77
+ RUBY
78
+ end
79
+
80
+ # Generates traceability header with generation metadata.
81
+ #
82
+ # @param behaviour_name [String]
83
+ # @param behaviour_version [String]
84
+ # @param rules [Array<Hash>]
85
+ # @param timestamp [String]
86
+ # @return [String]
87
+ def traceability_header(behaviour_name, behaviour_version, rules, timestamp)
88
+ <<~RUBY
89
+ # --- Traceability ---
90
+ # Source behaviour: #{behaviour_name}
91
+ # Behaviour version: #{behaviour_version}
92
+ # Generator: rosett-ai v#{RosettAi::VERSION}
93
+ # Generated at: #{timestamp}
94
+ # Rule count: #{rules.size}
95
+ # Rule IDs: #{rules.map { |r| r[:rule_id] }.join(', ')}
96
+ RUBY
97
+ end
98
+
99
+ # @return [String]
100
+ def ai_client_instructions
101
+ <<~RUBY
102
+ # --- AI Client Instructions ---
103
+ # This hook is a Claude Code PreToolUse hook. It reads JSON from stdin
104
+ # describing the tool invocation, checks the input against enforcement
105
+ # rules, and exits with:
106
+ # exit 0 — tool call allowed (or warn/log mode)
107
+ # exit 2 — tool call blocked (block mode, non-zero = reject)
108
+ # Stderr output is shown to the user as hook feedback.
109
+ # Do NOT modify this file — re-generate via rai_hook_install.
110
+ RUBY
111
+ end
112
+
113
+ # Generates the RULES constant with validated rule data.
114
+ #
115
+ # @param rules [Array<Hash>]
116
+ # @return [String]
117
+ def rule_constant(rules)
118
+ entries = rules.map { |r| format_rule_entry(r) }
119
+ <<~RUBY
120
+ RULES = [
121
+ #{entries.join(",\n")}
122
+ ].freeze
123
+ RUBY
124
+ end
125
+
126
+ # Formats a single rule entry for the RULES constant.
127
+ #
128
+ # @param rule [Hash]
129
+ # @return [String]
130
+ def format_rule_entry(rule)
131
+ desc = sanitize_string(rule[:description], 80)
132
+ msg = sanitize_string(rule[:message].to_s, 120)
133
+ tools = rule[:applies_to].map { |t| "'#{sanitize_string(t, 40)}'" }.join(', ')
134
+ behaviour = sanitize_string(rule[:behaviour], 100)
135
+ rule_id = sanitize_string(rule[:rule_id], 40)
136
+ [
137
+ " { behaviour: '#{behaviour}',",
138
+ " rule_id: '#{rule_id}',",
139
+ " priority: #{rule[:priority]},",
140
+ " pattern: #{format_regex(rule[:pattern])},",
141
+ " applies_to: [#{tools}],",
142
+ " action: :#{rule[:action]},",
143
+ " message: '#{msg}',",
144
+ " description: '#{desc}' }"
145
+ ].join("\n ")
146
+ end
147
+
148
+ # Formats a pattern string as a Regexp literal for the generated script.
149
+ #
150
+ # @param pattern [String]
151
+ # @return [String]
152
+ def format_regex(pattern)
153
+ "/#{pattern.gsub('/', '\\/')}/"
154
+ end
155
+
156
+ # Sanitizes a string for safe embedding in single-quoted Ruby strings.
157
+ # Backslashes must be escaped before quotes to avoid double-escaping.
158
+ #
159
+ # @param value [String]
160
+ # @param max_length [Integer]
161
+ # @return [String]
162
+ def sanitize_string(value, max_length)
163
+ value.to_s.tr("\n", ' ').strip[0, max_length].gsub('\\', '\\\\\\\\').gsub("'", "\\\\'")
164
+ end
165
+
166
+ # Generates the runtime enforcement logic body.
167
+ # Uses non-interpolating heredoc — this code runs at hook time, not generation time.
168
+ #
169
+ # @return [String]
170
+ def enforcement_body
171
+ <<~'RUBY'
172
+ require 'json'
173
+
174
+ input = JSON.parse($stdin.read)
175
+ tool_name = input['tool_name'].to_s
176
+ tool_input = input['tool_input'].to_s
177
+
178
+ violations = RULES.select do |rule|
179
+ rule[:applies_to].include?(tool_name) && rule[:pattern].match?(tool_input)
180
+ end
181
+
182
+ exit 0 if violations.empty?
183
+
184
+ violations.sort_by { |v| -v[:priority] }.each do |v|
185
+ $stderr.puts "[rai-enforce] #{v[:behaviour]}/#{v[:rule_id]} " \
186
+ "(P#{v[:priority]}, #{v[:action]}): #{v[:message].empty? ? v[:description] : v[:message]}"
187
+ end
188
+
189
+ # Block if any violation has action :block
190
+ blocked = violations.any? { |v| v[:action] == :block }
191
+ exit(blocked ? 2 : 0)
192
+ RUBY
193
+ end
194
+ end
195
+ end
196
+ end
197
+ end
@@ -0,0 +1,215 @@
1
+ # frozen_string_literal: true
2
+
3
+ # SPDX-License-Identifier: GPL-3.0-only
4
+ # Copyright (C) 2026 Hugo Antonio Sepulveda Manriquez / NeatNerds
5
+
6
+ module RosettAi
7
+ module Mcp
8
+ module Enforcement
9
+ # Validates enforcement blocks in behaviour rules.
10
+ #
11
+ # Checks each rule's enforcement block for correctness:
12
+ # - pattern present and compilable as regex
13
+ # - pattern not degenerate (would match everything)
14
+ # - applies_to present and non-empty
15
+ # - action is a valid enum value
16
+ #
17
+ # Invalid enforceable rules are downgraded to advisory with warnings.
18
+ # Rules without enforcement blocks are skipped (backward compatible).
19
+ #
20
+ # @example
21
+ # validator = Validator.new
22
+ # result = validator.validate(behaviour_data)
23
+ # result[:valid] # => [{ rule_id: "rule_001", ... }]
24
+ # result[:downgraded] # => [{ rule_id: "rule_002", reason: "..." }]
25
+ # result[:skipped] # => [{ rule_id: "rule_003" }]
26
+ #
27
+ # @author hugo
28
+ # @author claude
29
+ class Validator
30
+ VALID_TYPES = ['enforceable', 'advisory', 'informational'].freeze
31
+ VALID_ACTIONS = ['block', 'warn', 'log'].freeze
32
+
33
+ # Patterns that match everything or nearly everything — unsafe for hooks.
34
+ DEGENERATE_PATTERNS = [
35
+ /\A\.\*\z/, # .*
36
+ /\A\.\+\z/, # .+
37
+ /\A\\s\*\z/, # \s*
38
+ /\A\\S\*\z/, # \S*
39
+ /\A\\s\+\z/, # \s+
40
+ /\A\\S\+\z/, # \S+
41
+ /\A\.\*\?\z/, # .*?
42
+ /\A\.\+\?\z/, # .+?
43
+ /\A\(\.\*\)\z/, # (.*)
44
+ /\A\(\.\+\)\z/ # (.+)
45
+ ].freeze
46
+
47
+ # @!attribute [r] behaviour
48
+ # @return [String] behaviour name for error messages
49
+ # @!attribute [r] rule_id
50
+ # @return [String] current rule ID for error messages
51
+ RuleContext = Struct.new(:behaviour, :rule_id)
52
+
53
+ # Validates all enforcement blocks in a behaviour.
54
+ #
55
+ # @param behaviour [Hash] parsed behaviour YAML data
56
+ # @return [Hash] result with :valid, :downgraded, :skipped, :errors keys
57
+ def validate(behaviour)
58
+ @result = { valid: [], downgraded: [], skipped: [], errors: [] }
59
+ rules = behaviour['rules']
60
+ return @result unless rules.is_a?(Array)
61
+
62
+ behaviour_name = behaviour['name'].to_s
63
+ rules.each { |rule| validate_rule(rule, behaviour_name) }
64
+
65
+ @result
66
+ end
67
+
68
+ private
69
+
70
+ # Validates a single rule's enforcement block.
71
+ #
72
+ # @param rule [Hash] rule data from behaviour YAML
73
+ # @param behaviour_name [String] parent behaviour name
74
+ def validate_rule(rule, behaviour_name)
75
+ return unless rule.is_a?(Hash)
76
+
77
+ ctx = RuleContext.new(behaviour: behaviour_name, rule_id: rule['id'].to_s)
78
+ enforcement = rule['enforcement']
79
+
80
+ unless enforcement.is_a?(Hash)
81
+ @result[:skipped] << { rule_id: ctx.rule_id, behaviour: ctx.behaviour }
82
+ return
83
+ end
84
+
85
+ classify_enforcement(rule, enforcement, ctx)
86
+ end
87
+
88
+ # Classifies a rule that has an enforcement block.
89
+ #
90
+ # @param rule [Hash]
91
+ # @param enforcement [Hash]
92
+ # @param ctx [RuleContext]
93
+ def classify_enforcement(rule, enforcement, ctx)
94
+ type = enforcement['type'].to_s
95
+ unless VALID_TYPES.include?(type)
96
+ @result[:errors] << { rule_id: ctx.rule_id, behaviour: ctx.behaviour,
97
+ reason: "invalid enforcement type '#{type}' " \
98
+ "(must be one of: #{VALID_TYPES.join(', ')})" }
99
+ return
100
+ end
101
+
102
+ return record_non_enforceable(rule, ctx, type) unless type == 'enforceable'
103
+
104
+ validate_enforceable(rule, enforcement, ctx)
105
+ end
106
+
107
+ # Validates and classifies an enforceable rule.
108
+ #
109
+ # @param rule [Hash]
110
+ # @param enforcement [Hash]
111
+ # @param ctx [RuleContext]
112
+ def validate_enforceable(rule, enforcement, ctx)
113
+ warnings = collect_warnings(enforcement, ctx)
114
+
115
+ if warnings.empty?
116
+ @result[:valid] << build_valid_entry(rule, enforcement, ctx)
117
+ else
118
+ @result[:downgraded] << { rule_id: ctx.rule_id, behaviour: ctx.behaviour,
119
+ original_type: 'enforceable', downgraded_to: 'advisory',
120
+ warnings: warnings }
121
+ end
122
+ end
123
+
124
+ # Records advisory/informational rules (no field validation needed).
125
+ #
126
+ # @param rule [Hash]
127
+ # @param ctx [RuleContext]
128
+ # @param type [String]
129
+ def record_non_enforceable(rule, ctx, type)
130
+ @result[:valid] << { rule_id: rule['id'].to_s, behaviour: ctx.behaviour, type: type }
131
+ end
132
+
133
+ # Collects all warnings for an enforceable enforcement block.
134
+ #
135
+ # @param enforcement [Hash] the enforcement block
136
+ # @param ctx [RuleContext]
137
+ # @return [Array<String>]
138
+ def collect_warnings(enforcement, ctx)
139
+ prefix = "#{ctx.behaviour}/#{ctx.rule_id}"
140
+ [].concat(check_pattern(enforcement['pattern'], prefix))
141
+ .concat(check_applies_to(enforcement['applies_to'], prefix))
142
+ .concat(check_action(enforcement['action'], prefix))
143
+ end
144
+
145
+ # Validates the pattern field.
146
+ #
147
+ # @param pattern [String, nil]
148
+ # @param prefix [String] error message prefix
149
+ # @return [Array<String>]
150
+ def check_pattern(pattern, prefix)
151
+ return ["#{prefix}: pattern missing or empty"] if pattern.nil? || blank?(pattern)
152
+
153
+ Regexp.new(pattern)
154
+ return ["#{prefix}: pattern '#{pattern}' is degenerate (matches everything)"] if degenerate?(pattern)
155
+
156
+ []
157
+ rescue RegexpError => e
158
+ ["#{prefix}: pattern does not compile: #{e.message}"]
159
+ end
160
+
161
+ # Validates the applies_to field.
162
+ #
163
+ # @param applies_to [Array, nil]
164
+ # @param prefix [String] error message prefix
165
+ # @return [Array<String>]
166
+ def check_applies_to(applies_to, prefix)
167
+ return ["#{prefix}: applies_to missing or empty"] unless applies_to.is_a?(Array) && !applies_to.empty?
168
+
169
+ []
170
+ end
171
+
172
+ # Validates the action field.
173
+ #
174
+ # @param action [String, nil]
175
+ # @param prefix [String] error message prefix
176
+ # @return [Array<String>]
177
+ def check_action(action, prefix)
178
+ return [] if VALID_ACTIONS.include?(action)
179
+
180
+ allowed = VALID_ACTIONS.join(', ')
181
+ ["#{prefix}: action '#{action}' invalid (must be one of: #{allowed})"]
182
+ end
183
+
184
+ # Tests whether a string is blank (nil, empty, or whitespace-only).
185
+ #
186
+ # @param value [String]
187
+ # @return [Boolean]
188
+ def blank?(value)
189
+ value.is_a?(String) && value.strip.empty?
190
+ end
191
+
192
+ # Tests whether a pattern is degenerate (matches everything).
193
+ #
194
+ # @param pattern [String] regex pattern string
195
+ # @return [Boolean]
196
+ def degenerate?(pattern)
197
+ DEGENERATE_PATTERNS.any? { |dp| dp.match?(pattern.strip) }
198
+ end
199
+
200
+ # Builds a valid entry hash for an enforceable rule.
201
+ #
202
+ # @param rule [Hash]
203
+ # @param enforcement [Hash]
204
+ # @param ctx [RuleContext]
205
+ # @return [Hash]
206
+ def build_valid_entry(rule, enforcement, ctx)
207
+ { rule_id: ctx.rule_id, behaviour: ctx.behaviour, type: 'enforceable',
208
+ pattern: enforcement['pattern'], applies_to: enforcement['applies_to'],
209
+ action: enforcement['action'], message: enforcement['message'],
210
+ priority: rule['priority'] || 0, description: rule['description'].to_s.strip }
211
+ end
212
+ end
213
+ end
214
+ end
215
+ end