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,221 @@
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
+ # Multi-key authentication store with thread-safe reload.
9
+ #
10
+ # Manages a YAML keyfile with salted hashes, expiry dates,
11
+ # and enable/disable flags. File permissions are enforced
12
+ # at 0600 or 0400.
13
+ #
14
+ # @author hugo
15
+ # @author claude
16
+ class Keyfile
17
+ VERSION = '1.0'
18
+ MAX_PERMISSIONS = 0o600
19
+ DEFAULT_FILENAME = 'rosett-ai-mcp-keys.yml'
20
+ EXPIRY_WARNING_DAYS = 7
21
+
22
+ # @param path [String] path to the keyfile
23
+ def initialize(path)
24
+ @path = File.expand_path(path)
25
+ @mutex = Mutex.new
26
+ @keys = []
27
+ end
28
+
29
+ # Load and validate the keyfile.
30
+ #
31
+ # @return [self]
32
+ # @raise [RosettAi::McpError] if file is missing or invalid
33
+ def load!
34
+ @mutex.synchronize do
35
+ validate_file_exists
36
+ validate_permissions
37
+ parse_keyfile
38
+ end
39
+ self
40
+ end
41
+
42
+ # Thread-safe reload (for SIGHUP handler).
43
+ #
44
+ # @return [void]
45
+ def reload!
46
+ load!
47
+ rescue StandardError => e
48
+ warn "[rai-mcp] Keyfile reload failed: #{e.message}"
49
+ end
50
+
51
+ # Find a key entry matching the given plaintext.
52
+ #
53
+ # @param plaintext [String] the plaintext API key
54
+ # @return [Hash, nil] matched key entry or nil
55
+ def find_key(plaintext)
56
+ @mutex.synchronize do
57
+ @keys.find do |entry|
58
+ next unless entry[:enabled]
59
+ next if expired?(entry)
60
+
61
+ KeyHasher.verify_key(plaintext, entry[:key_hash])
62
+ end
63
+ end
64
+ end
65
+
66
+ # List enabled keys (without hashes).
67
+ #
68
+ # @return [Array<Hash>] enabled key entries
69
+ def enabled_keys
70
+ @mutex.synchronize do
71
+ @keys.select { |k| k[:enabled] }.map { |k| safe_entry(k) }
72
+ end
73
+ end
74
+
75
+ # Add a new key to the keyfile.
76
+ #
77
+ # @param name [String] key name
78
+ # @param key_hash [String] salted hash
79
+ # @param expires_at [String, nil] expiry date (ISO 8601)
80
+ # @return [void]
81
+ def add_key(name:, key_hash:, expires_at: nil)
82
+ @mutex.synchronize do
83
+ @keys << {
84
+ name: name,
85
+ algo: 'sha256_salt',
86
+ key_hash: key_hash,
87
+ created_at: Time.now.strftime('%Y-%m-%d'),
88
+ expires_at: expires_at,
89
+ enabled: true
90
+ }
91
+ write_keyfile
92
+ end
93
+ end
94
+
95
+ # Revoke (disable) a key by name.
96
+ #
97
+ # @param name [String] key name
98
+ # @return [Boolean] true if key was found and revoked
99
+ def revoke_key(name)
100
+ @mutex.synchronize do
101
+ entry = @keys.find { |k| k[:name] == name }
102
+ return false unless entry
103
+
104
+ entry[:enabled] = false
105
+ write_keyfile
106
+ true
107
+ end
108
+ end
109
+
110
+ # Rotate a key: revoke old, return new hash for replacement.
111
+ #
112
+ # @param name [String] key name
113
+ # @return [Hash, nil] new key entry or nil if not found
114
+ def rotate_key(name)
115
+ @mutex.synchronize do
116
+ old = @keys.find { |k| k[:name] == name }
117
+ return nil unless old
118
+
119
+ old[:enabled] = false
120
+ new_key, new_entry = build_replacement_key(name, old[:expires_at])
121
+ @keys << new_entry
122
+ write_keyfile
123
+ { plaintext: new_key, entry: safe_entry(new_entry) }
124
+ end
125
+ end
126
+
127
+ # Create an empty keyfile if missing.
128
+ #
129
+ # @param path [String] keyfile path
130
+ # @return [String] expanded path
131
+ def self.create_if_missing(path)
132
+ expanded = File.expand_path(path)
133
+ unless File.exist?(expanded)
134
+ dir = File.dirname(expanded)
135
+ FileUtils.mkdir_p(dir)
136
+ File.write(expanded, { 'version' => VERSION, 'keys' => [] }.to_yaml)
137
+ File.chmod(0o600, expanded)
138
+ end
139
+ expanded
140
+ end
141
+
142
+ private
143
+
144
+ def validate_file_exists
145
+ raise RosettAi::McpError, "Keyfile not found: #{@path}" unless File.exist?(@path)
146
+ end
147
+
148
+ def validate_permissions
149
+ mode = File.stat(@path).mode & 0o777
150
+ return if mode <= MAX_PERMISSIONS
151
+
152
+ raise RosettAi::McpError,
153
+ "Keyfile permissions too open: #{format('%04o', mode)} " \
154
+ "(max #{format('%04o', MAX_PERMISSIONS)}). Run: chmod 0600 #{@path}"
155
+ end
156
+
157
+ def parse_keyfile
158
+ data = YAML.safe_load_file(@path, permitted_classes: [Symbol])
159
+ raise RosettAi::McpError, 'Invalid keyfile format' unless data.is_a?(Hash)
160
+
161
+ @keys = Array(data['keys']).map { |k| symbolize_key(k) }
162
+ end
163
+
164
+ def symbolize_key(hash)
165
+ {
166
+ name: hash['name'],
167
+ algo: hash['algo'] || 'sha256_salt',
168
+ key_hash: hash['key_hash'],
169
+ created_at: hash['created_at'],
170
+ expires_at: hash['expires_at'],
171
+ enabled: hash.fetch('enabled', true)
172
+ }
173
+ end
174
+
175
+ def expired?(entry)
176
+ return false unless entry[:expires_at]
177
+
178
+ Time.parse(entry[:expires_at].to_s) < Time.now
179
+ rescue ArgumentError
180
+ false
181
+ end
182
+
183
+ def safe_entry(entry)
184
+ entry.except(:key_hash)
185
+ end
186
+
187
+ def write_keyfile
188
+ data = {
189
+ 'version' => VERSION,
190
+ 'keys' => @keys.map { |k| stringify_key(k) }
191
+ }
192
+ File.write(@path, data.to_yaml)
193
+ File.chmod(0o600, @path)
194
+ end
195
+
196
+ def build_replacement_key(name, expires_at)
197
+ plaintext = KeyHasher.generate_key
198
+ entry = {
199
+ name: name,
200
+ algo: 'sha256_salt',
201
+ key_hash: KeyHasher.hash_key(plaintext),
202
+ created_at: Time.now.strftime('%Y-%m-%d'),
203
+ expires_at: expires_at,
204
+ enabled: true
205
+ }
206
+ [plaintext, entry]
207
+ end
208
+
209
+ def stringify_key(entry)
210
+ {
211
+ 'name' => entry[:name],
212
+ 'algo' => entry[:algo],
213
+ 'key_hash' => entry[:key_hash],
214
+ 'created_at' => entry[:created_at],
215
+ 'expires_at' => entry[:expires_at],
216
+ 'enabled' => entry[:enabled]
217
+ }
218
+ end
219
+ end
220
+ end
221
+ end
@@ -0,0 +1,146 @@
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 Middleware
9
+ # Rack middleware for API key authentication.
10
+ #
11
+ # Supports multi-key keyfile or single env var modes.
12
+ # Returns 401 Unauthorized for invalid credentials.
13
+ #
14
+ # @author hugo
15
+ # @author claude
16
+ class Authentication
17
+ # @param app [#call] the next Rack application
18
+ # @param config [#type, #key_source, #keyfile_path, #api_key_env] auth config
19
+ def initialize(app, config: nil)
20
+ @app = app
21
+ @config = config
22
+ @keyfile = nil
23
+ validate_auth_type
24
+ load_keyfile if keyfile_mode?
25
+ install_sighup_handler if keyfile_mode?
26
+ end
27
+
28
+ # @param env [Hash] Rack environment
29
+ # @return [Array] Rack response triplet
30
+ def call(env)
31
+ case auth_type
32
+ when 'none'
33
+ env['rosett_ai.authenticated'] = false
34
+ @app.call(env)
35
+ when 'api_key'
36
+ authenticate_api_key(env)
37
+ else
38
+ unauthorized('Unsupported authentication type')
39
+ end
40
+ end
41
+
42
+ private
43
+
44
+ def auth_type
45
+ @config.respond_to?(:type) ? @config.type : 'api_key'
46
+ end
47
+
48
+ def validate_auth_type
49
+ type = auth_type
50
+ return if ['api_key', 'none'].include?(type)
51
+
52
+ warn "[rai-mcp] Unsupported auth type: #{type}, falling back to api_key"
53
+ end
54
+
55
+ def keyfile_mode?
56
+ return false unless @config.respond_to?(:resolved_key_source)
57
+
58
+ @config.resolved_key_source == 'keyfile'
59
+ end
60
+
61
+ def load_keyfile
62
+ path = @config.keyfile_path
63
+ return unless path
64
+
65
+ @keyfile = Keyfile.new(path)
66
+ @keyfile.load!
67
+ end
68
+
69
+ def install_sighup_handler
70
+ Signal.trap('HUP') { @keyfile&.reload! }
71
+ rescue ArgumentError
72
+ # Signal not supported on this platform
73
+ end
74
+
75
+ def authenticate_api_key(env)
76
+ if @keyfile
77
+ token = extract_bearer_token(env) # gitleaks:allow
78
+ return unauthorized('Missing Authorization header') unless token
79
+
80
+ authenticate_via_keyfile(env, token)
81
+ else
82
+ authenticate_via_env(env)
83
+ end
84
+ end
85
+
86
+ def authenticate_via_keyfile(env, token)
87
+ key_entry = @keyfile.find_key(token)
88
+ if key_entry
89
+ env['rosett_ai.authenticated'] = true
90
+ env['rosett_ai.client_id'] = key_entry[:name]
91
+ SecurityLogger.auth_success(key_entry[:name])
92
+ @app.call(env)
93
+ else
94
+ SecurityLogger.auth_failure('invalid_key')
95
+ unauthorized('Invalid API key')
96
+ end
97
+ end
98
+
99
+ def authenticate_via_env(env)
100
+ env_var = @config.respond_to?(:api_key_env) ? @config.api_key_env : 'RAI_MCP_API_KEY'
101
+ expected = ENV.fetch(env_var, nil)
102
+
103
+ unless expected
104
+ SecurityLogger.log(:warn, 'no_api_key_configured',
105
+ env_var: env_var,
106
+ message: 'HTTP request received with no API key configured — running unauthenticated')
107
+ env['rosett_ai.authenticated'] = false
108
+ return @app.call(env)
109
+ end
110
+
111
+ token = extract_bearer_token(env) # gitleaks:allow
112
+ return unauthorized('Missing Authorization header') unless token
113
+
114
+ if Rack::Utils.secure_compare(token, expected)
115
+ env['rosett_ai.authenticated'] = true
116
+ env['rosett_ai.client_id'] = 'env_key'
117
+ SecurityLogger.auth_success('env_key')
118
+ @app.call(env)
119
+ else
120
+ SecurityLogger.auth_failure('invalid_env_key')
121
+ unauthorized('Invalid API key')
122
+ end
123
+ end
124
+
125
+ def extract_bearer_token(env)
126
+ auth = env['HTTP_AUTHORIZATION']
127
+ return nil unless auth
128
+
129
+ scheme, token = auth.split(' ', 2) # gitleaks:allow
130
+ return nil unless scheme&.casecmp('bearer')&.zero?
131
+
132
+ token
133
+ end
134
+
135
+ def unauthorized(message)
136
+ body = JSON.generate(
137
+ jsonrpc: '2.0',
138
+ error: { code: -32_600, message: "Unauthorized: #{message}" },
139
+ id: nil
140
+ )
141
+ [401, { 'content-type' => 'application/json', 'www-authenticate' => 'Bearer' }, [body]]
142
+ end
143
+ end
144
+ end
145
+ end
146
+ end
@@ -0,0 +1,56 @@
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 Middleware
9
+ # Rack middleware that enforces application/json on POST requests.
10
+ #
11
+ # Returns 415 Unsupported Media Type for non-JSON POST bodies.
12
+ #
13
+ # @author hugo
14
+ # @author claude
15
+ class ContentType
16
+ # @param app [#call] the next Rack application
17
+ def initialize(app)
18
+ @app = app
19
+ end
20
+
21
+ # @param env [Hash] Rack environment
22
+ # @return [Array] Rack response triplet
23
+ def call(env)
24
+ return @app.call(env) unless post_request?(env)
25
+ return @app.call(env) if json_content_type?(env)
26
+
27
+ unsupported_media_type(env)
28
+ end
29
+
30
+ private
31
+
32
+ def post_request?(env)
33
+ env['REQUEST_METHOD'] == 'POST'
34
+ end
35
+
36
+ def json_content_type?(env)
37
+ content_type = env['CONTENT_TYPE'].to_s
38
+ content_type.start_with?('application/json')
39
+ end
40
+
41
+ def unsupported_media_type(env)
42
+ got = env['CONTENT_TYPE'].to_s
43
+ body = JSON.generate(
44
+ jsonrpc: '2.0',
45
+ error: {
46
+ code: -32_600,
47
+ message: "Content-Type must be application/json, got: #{got}"
48
+ },
49
+ id: nil
50
+ )
51
+ [415, { 'content-type' => 'application/json' }, [body]]
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,83 @@
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 Middleware
9
+ # Rack middleware for CORS header handling and preflight.
10
+ #
11
+ # Disabled by default. When enabled, handles OPTIONS preflight
12
+ # and adds CORS headers to responses.
13
+ #
14
+ # @author hugo
15
+ # @author claude
16
+ class Cors
17
+ # @param app [#call] the next Rack application
18
+ # @param config [#origins, #methods, #headers, #max_age] CORS config
19
+ DEFAULT_METHODS = ['POST', 'GET', 'DELETE'].freeze
20
+ DEFAULT_HEADERS = ['Content-Type', 'Authorization', 'Accept', 'Mcp-Session-Id'].freeze
21
+ DEFAULT_MAX_AGE = 86_400
22
+ CONFIG_FIELDS = [:origins, :methods, :headers, :max_age].freeze
23
+
24
+ def initialize(app, config: nil)
25
+ @app = app
26
+ @origins = resolve_config(config, :origins, [])
27
+ @methods = resolve_config(config, :methods, DEFAULT_METHODS)
28
+ @headers = resolve_config(config, :headers, DEFAULT_HEADERS)
29
+ @max_age = resolve_config(config, :max_age, DEFAULT_MAX_AGE)
30
+ end
31
+
32
+ # @param env [Hash] Rack environment
33
+ # @return [Array] Rack response triplet
34
+ def call(env)
35
+ return handle_preflight(env) if preflight?(env)
36
+
37
+ status, headers, body = @app.call(env)
38
+ add_cors_headers(headers, env)
39
+ [status, headers, body]
40
+ end
41
+
42
+ private
43
+
44
+ def preflight?(env)
45
+ env['REQUEST_METHOD'] == 'OPTIONS' && env['HTTP_ORIGIN']
46
+ end
47
+
48
+ def handle_preflight(env)
49
+ origin = env['HTTP_ORIGIN']
50
+ return [403, {}, []] unless origin_allowed?(origin)
51
+
52
+ [204, preflight_headers(origin), []]
53
+ end
54
+
55
+ def preflight_headers(origin)
56
+ {
57
+ 'access-control-allow-origin' => origin,
58
+ 'access-control-allow-methods' => @methods.join(', '),
59
+ 'access-control-allow-headers' => @headers.join(', '),
60
+ 'access-control-max-age' => @max_age.to_s
61
+ }
62
+ end
63
+
64
+ def add_cors_headers(headers, env)
65
+ origin = env['HTTP_ORIGIN']
66
+ return unless origin && origin_allowed?(origin)
67
+
68
+ headers['access-control-allow-origin'] = origin
69
+ end
70
+
71
+ def origin_allowed?(origin)
72
+ @origins.include?(origin)
73
+ end
74
+
75
+ def resolve_config(config, field, default)
76
+ return default unless CONFIG_FIELDS.include?(field) && config.respond_to?(field)
77
+
78
+ config.public_send(field) # rubocop:disable RosettAi/UnsafeSend -- field validated against CONFIG_FIELDS allowlist
79
+ end
80
+ end
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,73 @@
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 Middleware
9
+ # Rack middleware that validates Origin header against an allowlist.
10
+ #
11
+ # Supports glob patterns (e.g., 'http://localhost:*').
12
+ # Returns 403 Forbidden for disallowed origins.
13
+ # Localhost-only by default.
14
+ #
15
+ # @author hugo
16
+ # @author claude
17
+ class OriginValidation
18
+ DEFAULT_ORIGINS = ['http://localhost:*', 'http://127.0.0.1:*'].freeze
19
+
20
+ # @param app [#call] the next Rack application
21
+ # @param config [#allowed_origins, #strict_mode] origin config
22
+ def initialize(app, config: nil)
23
+ @app = app
24
+ @allowed_origins = config.respond_to?(:allowed_origins) ? config.allowed_origins : DEFAULT_ORIGINS
25
+ @strict_mode = config.respond_to?(:strict_mode) ? config.strict_mode : false
26
+ end
27
+
28
+ # @param env [Hash] Rack environment
29
+ # @return [Array] Rack response triplet
30
+ def call(env)
31
+ origin = env['HTTP_ORIGIN']
32
+ return handle_absent_origin(env) unless origin
33
+ return @app.call(env) if origin_allowed?(origin)
34
+
35
+ reject_origin(origin)
36
+ end
37
+
38
+ private
39
+
40
+ def handle_absent_origin(env)
41
+ if @strict_mode
42
+ forbidden('Origin header required (strict mode)')
43
+ else
44
+ @app.call(env)
45
+ end
46
+ end
47
+
48
+ def origin_allowed?(origin)
49
+ @allowed_origins.any? { |pattern| match_origin?(pattern, origin) }
50
+ end
51
+
52
+ def match_origin?(pattern, origin)
53
+ regex = Regexp.new("\\A#{Regexp.escape(pattern).gsub('\\*', '.*')}\\z")
54
+ regex.match?(origin)
55
+ end
56
+
57
+ def reject_origin(origin)
58
+ SecurityLogger.origin_rejected(origin)
59
+ forbidden("Origin not allowed: #{origin}")
60
+ end
61
+
62
+ def forbidden(message)
63
+ body = JSON.generate(
64
+ jsonrpc: '2.0',
65
+ error: { code: -32_600, message: message },
66
+ id: nil
67
+ )
68
+ [403, { 'content-type' => 'application/json' }, [body]]
69
+ end
70
+ end
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,106 @@
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 Middleware
9
+ # Rack middleware for token bucket rate limiting.
10
+ #
11
+ # Per-IP/per-key rate limiting with separate limits for
12
+ # authenticated and unauthenticated requests.
13
+ # Returns 429 Too Many Requests when exhausted.
14
+ #
15
+ # @author hugo
16
+ # @author claude
17
+ class RateLimit
18
+ DEFAULT_UNAUTH_RPM = 60
19
+ DEFAULT_AUTH_RPM = 300
20
+ BUCKET_WINDOW = 60 # seconds
21
+ CLEANUP_INTERVAL = 100
22
+
23
+ # @param app [#call] the next Rack application
24
+ # @param config [#unauthenticated_rpm, #authenticated_rpm] rate config
25
+ def initialize(app, config: nil)
26
+ @app = app
27
+ @unauth_rpm = config.respond_to?(:unauthenticated_rpm) ? config.unauthenticated_rpm : DEFAULT_UNAUTH_RPM
28
+ @auth_rpm = config.respond_to?(:authenticated_rpm) ? config.authenticated_rpm : DEFAULT_AUTH_RPM
29
+ @buckets = {}
30
+ @mutex = Mutex.new
31
+ @request_count = 0
32
+ end
33
+
34
+ # @param env [Hash] Rack environment
35
+ # @return [Array] Rack response triplet
36
+ def call(env)
37
+ key = bucket_key(env)
38
+ limit = rate_limit_for(env)
39
+
40
+ if allowed?(key, limit)
41
+ @app.call(env)
42
+ else
43
+ SecurityLogger.rate_limited(key)
44
+ too_many_requests
45
+ end
46
+ end
47
+
48
+ private
49
+
50
+ def bucket_key(env)
51
+ parts = [env['REMOTE_ADDR'] || 'unknown']
52
+ parts << env['HTTP_AUTHORIZATION'] if env['HTTP_AUTHORIZATION']
53
+ parts << env['HTTP_MCP_SESSION_ID'] if env['HTTP_MCP_SESSION_ID']
54
+ parts.join(':')
55
+ end
56
+
57
+ def rate_limit_for(env)
58
+ env['rosett_ai.authenticated'] ? @auth_rpm : @unauth_rpm
59
+ end
60
+
61
+ def allowed?(key, limit)
62
+ @mutex.synchronize do
63
+ @request_count += 1
64
+ cleanup_buckets if (@request_count % CLEANUP_INTERVAL).zero?
65
+
66
+ bucket = @buckets[key] ||= { tokens: limit, last_reset: Time.now }
67
+ reset_bucket(bucket, limit) if bucket_expired?(bucket)
68
+
69
+ if bucket[:tokens].positive?
70
+ bucket[:tokens] -= 1
71
+ true
72
+ else
73
+ false
74
+ end
75
+ end
76
+ end
77
+
78
+ def bucket_expired?(bucket)
79
+ Time.now - bucket[:last_reset] >= BUCKET_WINDOW
80
+ end
81
+
82
+ def reset_bucket(bucket, limit)
83
+ bucket[:tokens] = limit
84
+ bucket[:last_reset] = Time.now
85
+ end
86
+
87
+ def cleanup_buckets
88
+ now = Time.now
89
+ @buckets.delete_if { |_, bucket| now - bucket[:last_reset] >= BUCKET_WINDOW * 2 }
90
+ end
91
+
92
+ def too_many_requests
93
+ body = JSON.generate(
94
+ jsonrpc: '2.0',
95
+ error: {
96
+ code: -32_600,
97
+ message: 'Too many requests'
98
+ },
99
+ id: nil
100
+ )
101
+ [429, { 'content-type' => 'application/json', 'retry-after' => '60' }, [body]]
102
+ end
103
+ end
104
+ end
105
+ end
106
+ end