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,40 @@
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 Config
8
+ # Drop-in replacement for SecretResolver that masks all secret values
9
+ # instead of resolving them. Used by `config show` to display compiled
10
+ # JSON without exposing sensitive data.
11
+ #
12
+ # Follows the same interface as SecretResolver (resolve, resolve_all)
13
+ # and uses the same deterministic string parsing — no regex.
14
+ class MaskingSecretResolver
15
+ MASKED_VALUE = '***'
16
+
17
+ def resolve(value)
18
+ return value unless value.is_a?(String)
19
+ return value unless secret_reference?(value)
20
+
21
+ MASKED_VALUE
22
+ end
23
+
24
+ def resolve_all(obj)
25
+ case obj
26
+ when Hash then obj.transform_values { |v| resolve_all(v) }
27
+ when Array then obj.map { |v| resolve_all(v) }
28
+ when String then resolve(obj)
29
+ else obj
30
+ end
31
+ end
32
+
33
+ private
34
+
35
+ def secret_reference?(value)
36
+ value.start_with?(SecretResolver::SECRET_PREFIX) && value.end_with?(SecretResolver::SECRET_SUFFIX)
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,13 @@
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
+ # Configuration subsystem.
8
+ module Config
9
+ # Deprecated — use RosettAiEngine::Claude::ScopeRouter. Will be removed in v1.1.0.
10
+ # Only defined when rosett-ai-engine-claude gem is installed.
11
+ ScopeRouter = RosettAiEngine::Claude::ScopeRouter if defined?(RosettAiEngine::Claude::ScopeRouter)
12
+ end
13
+ end
@@ -0,0 +1,125 @@
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 Config
8
+ # Resolves ${secret:backend:key} references in configuration values.
9
+ #
10
+ # Security constraints:
11
+ # - NO regex anywhere — deterministic string parsing only (ReDoS prevention)
12
+ # - Single-pass resolution — resolved values are never re-scanned
13
+ # - Fail loudly — raises on missing secrets, never returns nil
14
+ # - File backend: 0600 permissions, owned by Process.uid, 64 KiB cap
15
+ # - Path backend: rejects ".." traversal
16
+ class SecretResolver
17
+ SECRET_PREFIX = '${secret:'
18
+ SECRET_SUFFIX = '}'
19
+ VALID_BACKENDS = ['env', 'file', 'path'].freeze
20
+ MAX_FILE_SIZE = 65_536 # 64 KiB
21
+
22
+ class SecretError < RosettAi::Error; end
23
+
24
+ # Resolve a single secret reference string.
25
+ #
26
+ # Returns the original value unchanged if it is not a secret reference.
27
+ #
28
+ # @param value [Object] a string potentially containing a secret reference, or any other object
29
+ # @return [Object] the resolved secret value, or the original value if not a reference
30
+ # @raise [SecretError] if the secret backend is unknown or the secret cannot be resolved
31
+ def resolve(value)
32
+ return value unless value.is_a?(String)
33
+ return value unless secret_reference?(value)
34
+
35
+ inner = value[SECRET_PREFIX.length..-2]
36
+ backend, key = inner.split(':', 2)
37
+
38
+ validate_reference!(backend, key)
39
+
40
+ case backend
41
+ when 'env' then resolve_env(key)
42
+ when 'file' then resolve_file(key)
43
+ when 'path' then resolve_path(key)
44
+ end
45
+ end
46
+
47
+ # Walk a nested structure and resolve all secret references.
48
+ #
49
+ # Single-pass: resolved values are never re-scanned.
50
+ #
51
+ # @param obj [Hash, Array, String, Object] nested structure to resolve
52
+ # @return [Object] structure with all secret references resolved
53
+ def resolve_all(obj)
54
+ case obj
55
+ when Hash then obj.transform_values { |v| resolve_all(v) }
56
+ when Array then obj.map { |v| resolve_all(v) }
57
+ when String then resolve(obj)
58
+ else obj
59
+ end
60
+ end
61
+
62
+ private
63
+
64
+ def secret_reference?(value)
65
+ value.start_with?(SECRET_PREFIX) && value.end_with?(SECRET_SUFFIX)
66
+ end
67
+
68
+ def validate_reference!(backend, key)
69
+ raise SecretError, 'empty secret reference' if backend.nil? || backend.empty? || key.nil? || key.empty?
70
+
71
+ return if VALID_BACKENDS.include?(backend)
72
+
73
+ raise SecretError, "unknown secret backend: #{backend}. Valid: #{VALID_BACKENDS.join(', ')}"
74
+ end
75
+
76
+ def resolve_env(key)
77
+ ENV.fetch(key) do
78
+ raise SecretError, "environment variable #{key} is not set"
79
+ end
80
+ end
81
+
82
+ def resolve_file(path)
83
+ reject_traversal!(path)
84
+
85
+ expanded = File.expand_path(path)
86
+ raise SecretError, "secret file not found: #{expanded}" unless File.exist?(expanded)
87
+
88
+ stat = File.lstat(expanded)
89
+ validate_file_permissions!(expanded, stat)
90
+
91
+ content = File.read(expanded, MAX_FILE_SIZE + 1)
92
+ raise SecretError, "secret file #{expanded} exceeds 64 KiB" if content.bytesize > MAX_FILE_SIZE
93
+
94
+ content.chomp
95
+ end
96
+
97
+ def resolve_path(path)
98
+ reject_traversal!(path)
99
+
100
+ expanded = File.expand_path(path)
101
+ raise SecretError, "path not found: #{expanded}" unless File.exist?(expanded)
102
+
103
+ expanded
104
+ end
105
+
106
+ def reject_traversal!(path)
107
+ return unless path.include?('..')
108
+
109
+ raise SecretError, "path traversal rejected in secret reference: #{path}"
110
+ end
111
+
112
+ def validate_file_permissions!(expanded, stat)
113
+ unless (stat.mode & 0o777) == 0o600
114
+ raise SecretError,
115
+ "secret file #{expanded} permissions #{format('%04o', stat.mode & 0o777)} are not 0600"
116
+ end
117
+
118
+ return if stat.uid == Process.uid
119
+
120
+ raise SecretError,
121
+ "secret file #{expanded} owned by uid #{stat.uid}, expected #{Process.uid}"
122
+ end
123
+ end
124
+ end
125
+ end
@@ -0,0 +1,119 @@
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'
7
+ require 'pathname'
8
+
9
+ module RosettAi
10
+ # Manages hierarchical configuration for Rosett-AI
11
+ # Supports global -> group -> project configuration inheritance
12
+ class Configuration
13
+ SETTINGS_FILE = 'settings.json'
14
+ LOCAL_SETTINGS_FILE = 'settings.local.json'
15
+ BEHAVIOUR_DIR = 'conf/behaviour'
16
+ SCHEMA_DIR = 'conf/schemas'
17
+
18
+ attr_reader :root_path
19
+
20
+ # Initializes a new Configuration instance.
21
+ #
22
+ # @param root_path [String, Pathname, nil] path to the Rosett-AI root directory;
23
+ # defaults to {RosettAi.root} when nil
24
+ # @return [Configuration] a new configuration instance
25
+ def initialize(root_path: nil)
26
+ @root_path = resolve_root_path(root_path)
27
+ end
28
+
29
+ # Returns the parsed global settings from settings.json.
30
+ #
31
+ # @return [Hash] global settings hash (memoized)
32
+ def settings
33
+ @settings ||= load_settings
34
+ end
35
+
36
+ # Returns the parsed local overrides from settings.local.json.
37
+ #
38
+ # @return [Hash] local settings hash (memoized)
39
+ def local_settings
40
+ @local_settings ||= load_local_settings
41
+ end
42
+
43
+ # Returns global settings deep-merged with local overrides.
44
+ #
45
+ # @return [Hash] merged settings hash (memoized)
46
+ def merged_settings
47
+ @merged_settings ||= deep_merge(settings, local_settings)
48
+ end
49
+
50
+ # Returns the list of behaviour YAML files found in the behaviour directory.
51
+ #
52
+ # @return [Array<String>] absolute paths to behaviour YAML files (memoized)
53
+ def behaviour_files
54
+ @behaviour_files ||= Dir.glob(behaviour_path.join('*.yml'))
55
+ end
56
+
57
+ # Returns the path to the behaviour configuration directory.
58
+ #
59
+ # @return [Pathname] path to conf/behaviour/
60
+ def behaviour_path
61
+ root_path.join(BEHAVIOUR_DIR)
62
+ end
63
+
64
+ # Returns the path to the JSON Schema directory.
65
+ #
66
+ # @return [Pathname] path to conf/schemas/
67
+ def schema_path
68
+ root_path.join(SCHEMA_DIR)
69
+ end
70
+
71
+ # Clears all memoized state so the next access re-reads from disk.
72
+ #
73
+ # @return [void]
74
+ def reload!
75
+ @settings = nil
76
+ @local_settings = nil
77
+ @merged_settings = nil
78
+ @behaviour_files = nil
79
+ end
80
+
81
+ private
82
+
83
+ def resolve_root_path(path)
84
+ Pathname.new(path || RosettAi.root)
85
+ end
86
+
87
+ def load_settings
88
+ load_json_file(SETTINGS_FILE)
89
+ end
90
+
91
+ def load_local_settings
92
+ load_json_file(LOCAL_SETTINGS_FILE)
93
+ end
94
+
95
+ def load_json_file(filename)
96
+ file_path = root_path.join(filename)
97
+ return {} unless file_path.exist?
98
+
99
+ JSON.parse(file_path.read)
100
+ rescue JSON::ParserError => e
101
+ raise ConfigurationError, "Invalid JSON in #{filename}: #{e.message}"
102
+ end
103
+
104
+ def deep_merge(base, overlay)
105
+ return base if overlay.to_h.empty?
106
+
107
+ base.merge(overlay) do |_key, base_val, overlay_val|
108
+ merge_values(base_val, overlay_val)
109
+ end
110
+ end
111
+
112
+ def merge_values(base_val, overlay_val)
113
+ return deep_merge(base_val, overlay_val) if base_val.is_a?(Hash) && overlay_val.is_a?(Hash)
114
+ return base_val | overlay_val if base_val.is_a?(Array) && overlay_val.is_a?(Array)
115
+
116
+ overlay_val
117
+ end
118
+ end
119
+ end
@@ -0,0 +1,60 @@
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 'faraday'
7
+
8
+ module RosettAi
9
+ module Content
10
+ # Faraday-based HTTP client for the NeatNerds content server.
11
+ #
12
+ # License key is sent as Bearer token. All requests use HTTPS.
13
+ # Gracefully handles offline scenarios.
14
+ class ContentClient
15
+ DEFAULT_BASE_URL = 'https://content.neatnerds.be/v1/'
16
+
17
+ def initialize(license_key:, base_url: DEFAULT_BASE_URL)
18
+ @license_key = license_key
19
+ @connection = Faraday.new(url: base_url) do |conn|
20
+ conn.request :authorization, 'Bearer', license_key
21
+ conn.adapter Faraday.default_adapter
22
+ end
23
+ end
24
+
25
+ def catalog(tier:)
26
+ response = @connection.get('packs', tier: tier)
27
+ raise RosettAi::ContentError, "Server error: #{response.status}" unless response.success?
28
+
29
+ YAML.safe_load(response.body, permitted_classes: [])
30
+ rescue Faraday::ConnectionFailed, Faraday::TimeoutError => e
31
+ raise RosettAi::ContentError, "Cannot reach content server: #{e.message}"
32
+ end
33
+
34
+ def download(pack_name, version:)
35
+ response = @connection.get("packs/#{pack_name}/#{version}/download")
36
+ raise RosettAi::ContentError, "Download failed: #{response.status}" unless response.success?
37
+
38
+ response.body
39
+ rescue Faraday::ConnectionFailed, Faraday::TimeoutError => e
40
+ raise RosettAi::ContentError, "Cannot reach content server: #{e.message}"
41
+ end
42
+
43
+ def download_signature(pack_name, version:)
44
+ response = @connection.get("packs/#{pack_name}/#{version}/signature")
45
+ raise RosettAi::ContentError, "Signature download failed: #{response.status}" unless response.success?
46
+
47
+ response.body
48
+ rescue Faraday::ConnectionFailed, Faraday::TimeoutError => e
49
+ raise RosettAi::ContentError, "Cannot reach content server: #{e.message}"
50
+ end
51
+
52
+ def available?(pack_name)
53
+ response = @connection.head("packs/#{pack_name}")
54
+ response.success?
55
+ rescue Faraday::ConnectionFailed, Faraday::TimeoutError
56
+ false
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,117 @@
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 'rubygems/package'
7
+ require 'zlib'
8
+ require 'ed25519'
9
+ require 'tempfile'
10
+ require 'fileutils'
11
+
12
+ module RosettAi
13
+ module Content
14
+ # Verifies Ed25519 signatures and extracts content pack tarballs.
15
+ #
16
+ # Security:
17
+ # - Signature verified BEFORE extraction
18
+ # - Path traversal entries (../) rejected
19
+ # - Atomic install: extract to temp dir, validate manifest, then move
20
+ # - Uses same PUBLIC_KEY_HEX as LicenseKey for verification
21
+ class PackInstaller
22
+ SAFE_DIR_MODE = 0o700
23
+
24
+ # Verifies signature and installs a content pack atomically.
25
+ #
26
+ # @param tarball_bytes [String] raw .tar.gz bytes
27
+ # @param signature_bytes [String] Ed25519 signature bytes
28
+ # @param pack_name [String] target pack directory name
29
+ # @return [PackManifest] manifest from the installed pack
30
+ # @raise [RosettAi::ContentError] if signature is invalid or manifest is missing
31
+ def install(tarball_bytes, signature_bytes, pack_name)
32
+ verify_signature!(tarball_bytes, signature_bytes)
33
+
34
+ temp_dir = Dir.mktmpdir('rosett-ai-pack-')
35
+ extract_tarball(tarball_bytes, temp_dir)
36
+ manifest = validate_extracted_manifest(temp_dir)
37
+ move_to_final(temp_dir, pack_name)
38
+ manifest
39
+ rescue StandardError
40
+ FileUtils.rm_rf(temp_dir) if temp_dir && Dir.exist?(temp_dir)
41
+ raise
42
+ end
43
+
44
+ private
45
+
46
+ def verify_signature!(tarball_bytes, signature_bytes)
47
+ public_hex = RosettAi::Licensing::LicenseKey::PUBLIC_KEY_HEX
48
+ verify_key = Ed25519::VerifyKey.new([public_hex].pack('H*'))
49
+ verify_key.verify(signature_bytes, tarball_bytes)
50
+ rescue Ed25519::VerifyError
51
+ raise RosettAi::ContentError, 'Content pack signature invalid. Pack not installed.'
52
+ end
53
+
54
+ def extract_tarball(tarball_bytes, dest_dir)
55
+ io = StringIO.new(tarball_bytes)
56
+ gz = Zlib::GzipReader.new(io)
57
+ Gem::Package::TarReader.new(gz) do |tar|
58
+ tar.each do |entry|
59
+ reject_traversal!(entry.full_name)
60
+ extract_entry(entry, dest_dir)
61
+ end
62
+ end
63
+ ensure
64
+ gz&.close
65
+ end
66
+
67
+ def reject_traversal!(path)
68
+ # Reject parent directory traversal
69
+ raise RosettAi::ContentError, "Path traversal rejected in content pack: #{path}" if path.include?('..')
70
+
71
+ # Reject absolute paths (could escape extraction directory)
72
+ raise RosettAi::ContentError, "Absolute path rejected in content pack: #{path}" if path.start_with?('/')
73
+
74
+ # Reject paths that start with ~ (home directory expansion)
75
+ raise RosettAi::ContentError, "Home directory path rejected in content pack: #{path}" if path.start_with?('~')
76
+
77
+ # Reject paths containing null bytes (path truncation attack)
78
+ return unless path.include?("\x00")
79
+
80
+ raise RosettAi::ContentError, "Null byte rejected in content pack path: #{path.inspect}"
81
+ end
82
+
83
+ def extract_entry(entry, dest_dir)
84
+ target = File.join(dest_dir, entry.full_name)
85
+
86
+ # Canonicalize and verify target stays within extraction directory
87
+ canonical_target = File.expand_path(target)
88
+ canonical_dest = File.expand_path(dest_dir)
89
+ unless canonical_target.start_with?("#{canonical_dest}/")
90
+ raise RosettAi::ContentError, "Path escape rejected in content pack: #{entry.full_name}"
91
+ end
92
+
93
+ if entry.directory?
94
+ FileUtils.mkdir_p(target, mode: SAFE_DIR_MODE)
95
+ elsif entry.file?
96
+ FileUtils.mkdir_p(File.dirname(target), mode: SAFE_DIR_MODE)
97
+ File.write(target, entry.read, perm: 0o644)
98
+ end
99
+ # Symlinks are silently ignored (not extracted) for security
100
+ end
101
+
102
+ def validate_extracted_manifest(temp_dir)
103
+ manifest_path = File.join(temp_dir, 'manifest.yml')
104
+ raise RosettAi::ContentError, 'Content pack missing manifest.yml' unless File.exist?(manifest_path)
105
+
106
+ PackManifest.from_file(manifest_path)
107
+ end
108
+
109
+ def move_to_final(temp_dir, pack_name)
110
+ final_dir = RosettAi.paths.premium_content_dir.join(pack_name)
111
+ FileUtils.rm_rf(final_dir.to_s) if final_dir.exist?
112
+ FileUtils.mkdir_p(final_dir.dirname.to_s, mode: SAFE_DIR_MODE)
113
+ FileUtils.mv(temp_dir, final_dir.to_s)
114
+ end
115
+ end
116
+ end
117
+ end
@@ -0,0 +1,50 @@
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 Content
10
+ # Loads, validates, and provides access to content pack manifest data.
11
+ #
12
+ # Manifest files are YAML (parsed with safe_load only) and validated
13
+ # against the content_pack_manifest_schema.json schema.
14
+ class PackManifest
15
+ attr_reader :data
16
+
17
+ def initialize(data)
18
+ @data = data
19
+ validate!
20
+ end
21
+
22
+ def self.from_file(path)
23
+ data = RosettAi::YamlLoader.load_file(path)
24
+ new(data)
25
+ end
26
+
27
+ def name = data['name']
28
+ def version = data['version']
29
+ def tier_required = data['tier_required']
30
+ def description = data['description']
31
+ def author = data['author']
32
+ def contents = data['contents'] || {}
33
+ def behaviour_files = contents['behaviours'] || []
34
+ def design_files = contents['designs'] || []
35
+
36
+ private
37
+
38
+ def validate!
39
+ schema_path = RosettAi.root.join('conf', 'schemas', 'content_pack_manifest_schema.json')
40
+ schema = JSON.parse(schema_path.read)
41
+ schemer = JSONSchemer.schema(schema)
42
+ errors = schemer.validate(data).to_a
43
+ return if errors.empty?
44
+
45
+ messages = errors.map { |err| "#{err['data_pointer']}: #{err['type']}" }
46
+ raise RosettAi::ContentError, "Invalid manifest: #{messages.join(', ')}"
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,68 @@
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 Content
8
+ # Tracks installed content packs via filesystem scanning.
9
+ #
10
+ # No separate registry file — the filesystem IS the registry.
11
+ # Each pack directory under ~/.config/rosett-ai/premium/ contains a
12
+ # manifest.yml that describes the pack.
13
+ class PackRegistry
14
+ # Lists all installed content packs by scanning the premium directory.
15
+ #
16
+ # @return [Array<PackManifest>] manifests for each installed pack
17
+ def installed
18
+ return [] unless premium_dir.exist?
19
+
20
+ Dir.children(premium_dir.to_s).filter_map do |entry|
21
+ dir = premium_dir.join(entry)
22
+ manifest_path = dir.join('manifest.yml')
23
+ next unless dir.directory? && manifest_path.exist?
24
+
25
+ PackManifest.from_file(manifest_path.to_s)
26
+ rescue RosettAi::ContentError
27
+ nil
28
+ end
29
+ end
30
+
31
+ # Whether a pack with the given name is installed.
32
+ #
33
+ # @param pack_name [String] pack directory name
34
+ # @return [Boolean]
35
+ def installed?(pack_name)
36
+ manifest_path = premium_dir.join(pack_name, 'manifest.yml')
37
+ manifest_path.exist?
38
+ end
39
+
40
+ # Returns the installed version of a pack.
41
+ #
42
+ # @param pack_name [String] pack directory name
43
+ # @return [String, nil] version string or nil if not installed
44
+ def version(pack_name)
45
+ manifest_path = premium_dir.join(pack_name, 'manifest.yml')
46
+ return nil unless manifest_path.exist?
47
+
48
+ manifest = PackManifest.from_file(manifest_path.to_s)
49
+ manifest.version
50
+ end
51
+
52
+ # Removes an installed pack and its directory.
53
+ #
54
+ # @param pack_name [String] pack directory name
55
+ # @return [void]
56
+ def remove!(pack_name)
57
+ dir = premium_dir.join(pack_name)
58
+ FileUtils.rm_rf(dir.to_s) if dir.exist?
59
+ end
60
+
61
+ private
62
+
63
+ def premium_dir
64
+ RosettAi.paths.premium_content_dir
65
+ end
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,50 @@
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 ContentPacks
8
+ # MCP-facing facade for content pack operations.
9
+ #
10
+ # Delegates to {Content::PackRegistry} and {Content::PackInstaller}
11
+ # for the actual content pack logic, providing the simplified
12
+ # interface expected by {Mcp::Tools::ContentTool}.
13
+ #
14
+ # @author hugo
15
+ # @author claude
16
+ class Manager
17
+ def initialize
18
+ @registry = Content::PackRegistry
19
+ @installer = Content::PackInstaller.new
20
+ end
21
+
22
+ # Lists installed content packs.
23
+ #
24
+ # @return [Array<Hash>] installed packs with :name and :version
25
+ def list
26
+ @registry.installed.map do |pack|
27
+ { name: pack.name, version: pack.version }
28
+ end
29
+ end
30
+
31
+ # Installs a content pack by name.
32
+ #
33
+ # @param pack_name [String] name of the pack to install
34
+ # @return [Boolean] true on success
35
+ def install(pack_name)
36
+ @installer.install(pack_name)
37
+ pack_name
38
+ end
39
+
40
+ # Updates all installed content packs.
41
+ #
42
+ # @return [Array<String>] names of updated packs
43
+ def update_all
44
+ @registry.installed.filter_map do |pack|
45
+ @installer.update(pack.name) ? pack.name : nil
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end