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.
- checksums.yaml +7 -0
- data/.ai-provenance.yml +119 -0
- data/.debride_whitelist +186 -0
- data/.fasterer.yml +29 -0
- data/.mdl_style.rb +10 -0
- data/.mdlrc +3 -0
- data/.mutant.yml +49 -0
- data/.namespace-allowlist +42 -0
- data/.reek.yml +1040 -0
- data/.rosett-ai/config.yml +3 -0
- data/.rspec +5 -0
- data/.rubocop.yml +380 -0
- data/.ruby-version +1 -0
- data/.yamllint +51 -0
- data/.yardopts +12 -0
- data/AI-DISCLOSURE.md +48 -0
- data/CHANGELOG.md +519 -0
- data/CLAUDE.md +141 -0
- data/CONTRIBUTING.md +734 -0
- data/INSTALL.md +154 -0
- data/LICENSE +674 -0
- data/LICENSE.md +675 -0
- data/QUICKSTART.md +73 -0
- data/README.md +366 -0
- data/Rakefile +200 -0
- data/SECURITY.md +114 -0
- data/bin/rai +1 -0
- data/cliff.toml +52 -0
- data/conf/adopt_redactions.yml +8 -0
- data/conf/behaviour/.gitkeep +0 -0
- data/conf/compliance/cra_rules.yml +25 -0
- data/conf/compliance/license_rules.yml +20 -0
- data/conf/design/aaif_alignment.yml +181 -0
- data/conf/design/ab_testing.yml +172 -0
- data/conf/design/accessibility.yml +84 -0
- data/conf/design/ai_authorship.yml +210 -0
- data/conf/design/ai_provenance.yml +224 -0
- data/conf/design/ai_tool_configuration.yml +207 -0
- data/conf/design/architecture.yml +139 -0
- data/conf/design/autocompletion.yml +115 -0
- data/conf/design/backward_compatibility.yml +112 -0
- data/conf/design/behaviour_composition.yml +246 -0
- data/conf/design/build_rake_extraction.yml +57 -0
- data/conf/design/ci_pipeline.yml +100 -0
- data/conf/design/claude_code_configuration.yml +157 -0
- data/conf/design/compiler.yml +128 -0
- data/conf/design/comply.yml +153 -0
- data/conf/design/content_packs.yml +84 -0
- data/conf/design/desktop_integration.yml +289 -0
- data/conf/design/distribution.yml +216 -0
- data/conf/design/doctor.yml +184 -0
- data/conf/design/documentation.yml +152 -0
- data/conf/design/engine_architecture.yml +257 -0
- data/conf/design/error_handling.yml +103 -0
- data/conf/design/feature_flags.yml +142 -0
- data/conf/design/git_hooks.yml +165 -0
- data/conf/design/gui_plugins.yml +475 -0
- data/conf/design/i18n.yml +84 -0
- data/conf/design/integration_testing.yml +56 -0
- data/conf/design/licensing_system.yml +88 -0
- data/conf/design/lifecycle_management.yml +208 -0
- data/conf/design/mcp_integration.yml +207 -0
- data/conf/design/mcp_settings.yml +126 -0
- data/conf/design/migration.yml +56 -0
- data/conf/design/monitoring_observability.yml +194 -0
- data/conf/design/namespace_cleanup.yml +145 -0
- data/conf/design/plugin_test_segregation.yml +145 -0
- data/conf/design/policy_management.yml +229 -0
- data/conf/design/project_management.yml +183 -0
- data/conf/design/rai_mcp_asset_discovery.yml +164 -0
- data/conf/design/rai_mcp_server.yml +605 -0
- data/conf/design/release_management.yml +117 -0
- data/conf/design/retrofit.yml +199 -0
- data/conf/design/retrospective_analyzer.yml +79 -0
- data/conf/design/scope_hierarchy.yml +352 -0
- data/conf/design/security.yml +115 -0
- data/conf/design/session_retrospective.yml +85 -0
- data/conf/design/smart_ui_feedback.yml +89 -0
- data/conf/design/structured_logging.yml +148 -0
- data/conf/design/styles.yml +123 -0
- data/conf/design/test_peer_review.yml +89 -0
- data/conf/design/testing.yml +136 -0
- data/conf/design/threat_model.yml +108 -0
- data/conf/design/ui_framework.yml +111 -0
- data/conf/design/usage_optimization.yml +122 -0
- data/conf/design/version_management.yml +60 -0
- data/conf/design/workflow.yml +227 -0
- data/conf/mcp/server_defaults.yml +42 -0
- data/conf/mcp/trust.yml +21 -0
- data/conf/packaging/core.yml +12 -0
- data/conf/packaging/gtk4.yml +11 -0
- data/conf/packaging/qt6.yml +11 -0
- data/conf/policy/default_deny_list.yml +197 -0
- data/conf/review/cli-command-audit.yml +857 -0
- data/conf/review/design-docs.yml +1064 -0
- data/conf/review/design-questionnaire.yml +153 -0
- data/conf/review/questionnaire.yml +146 -0
- data/conf/review/rosett-ai-core.yml +2919 -0
- data/conf/schemas/ai_config_schema.json +73 -0
- data/conf/schemas/behaviour_schema.json +132 -0
- data/conf/schemas/compliance_rule_schema.json +63 -0
- data/conf/schemas/content_pack_manifest_schema.json +51 -0
- data/conf/schemas/design_schema.json +210 -0
- data/conf/schemas/engine_manifest_schema.json +144 -0
- data/conf/schemas/lockfile_schema.json +74 -0
- data/conf/schemas/mcp_server_schema.json +48 -0
- data/conf/schemas/packaging_schema.json +70 -0
- data/conf/schemas/policy_schema.json +85 -0
- data/conf/schemas/provenance_schema.json +84 -0
- data/conf/schemas/rai_config_schema.json +56 -0
- data/conf/schemas/rai_project_schema.json +20 -0
- data/conf/schemas/scope_hierarchy_schema.json +49 -0
- data/conf/schemas/target_schema.json +67 -0
- data/conf/schemas/tooling_schema.json +65 -0
- data/conf/schemas/workflow_schema.json +112 -0
- data/conf/targets/agents_md.yml +17 -0
- data/conf/targets/claude.yml +12 -0
- data/conf/tooling/tools.yml +58 -0
- data/dist/rosett-ai-mcp.service +48 -0
- data/dist/rosett-ai-mcp.yml.default +45 -0
- data/doc/AAIF_POSITIONING.md +58 -0
- data/doc/ADOPT.md +224 -0
- data/doc/AI_PROVENANCE.md +139 -0
- data/doc/ARCHITECTURE.md +920 -0
- data/doc/BEHAVIOUR.md +409 -0
- data/doc/BUILD.md +138 -0
- data/doc/CI_CD_RECIPES.md +171 -0
- data/doc/CLAUDE_SESSIONS_MOVED.md +16 -0
- data/doc/COMMAND_ANALYSIS.md +229 -0
- data/doc/CONFIGURATION.md +281 -0
- data/doc/DESIGN_AUDIT.md +235 -0
- data/doc/DESIGN_PEER_REVIEW.md +771 -0
- data/doc/DESKTOP.md +447 -0
- data/doc/ENGINES.md +567 -0
- data/doc/ENGINE_DEVELOPMENT_GUIDE.md +417 -0
- data/doc/FEATURE_AUDIT.md +218 -0
- data/doc/IMPLEMENTATION_PLAN.md +669 -0
- data/doc/INCIDENT_REPORT_2026-02-02.md +251 -0
- data/doc/MIGRATION_GUIDE.md +88 -0
- data/doc/PACKAGING.md +232 -0
- data/doc/PROJECT_DASHBOARD.md +153 -0
- data/doc/PULP_DEPLOYMENT.md +164 -0
- data/doc/QUALITY_FIX_SUMMARY.md +110 -0
- data/doc/QUICK_START.md +162 -0
- data/doc/REEK_CONFIGURATION.md +166 -0
- data/doc/REFERENCE.md +253 -0
- data/doc/REFERENCES.md +324 -0
- data/doc/SECURITY_REVIEW_CHECKLIST.md +72 -0
- data/doc/SESSION_2026-02-28_GTK4_HARDENING.md +359 -0
- data/doc/SETUP.md +202 -0
- data/doc/TEST_PEER_REVIEW.md +152 -0
- data/doc/THREAT_MODEL.md +230 -0
- data/doc/USAGE.md +545 -0
- data/doc/USER_MANUAL.md +585 -0
- data/doc/ai_test_review_checklist.md +110 -0
- data/doc/changes/2026-02-18-packaging-fpm.md +155 -0
- data/doc/changes/2026-02-19-testing-infrastructure.md +221 -0
- data/doc/changes/2026-02-20-security-implementation.md +281 -0
- data/doc/changes/2026-02-20-styles-implementation.md +220 -0
- data/doc/changes/2026-02-21-architecture-completion.md +95 -0
- data/doc/changes/2026-02-21-architecture-ui-layer.md +253 -0
- data/doc/changes/2026-02-21-cc-config-implementation.md +108 -0
- data/doc/changes/2026-02-21-ci-pipeline-implementation.md +214 -0
- data/doc/changes/2026-02-21-compiler-multi-target-pipeline.md +241 -0
- data/doc/changes/2026-02-21-config-design-show-commands.md +61 -0
- data/doc/changes/2026-02-21-design-implementation-overview.md +455 -0
- data/doc/changes/2026-02-21-lifecycle-management.md +196 -0
- data/doc/changes/2026-02-21-path-resolver.md +128 -0
- data/doc/changes/2026-02-24-ci-tmpdir-mutant-fetch.md +45 -0
- data/doc/changes/2026-03-01-ci-bundler-strategy.md +120 -0
- data/doc/changes/2026-03-20-security-hardening-phase2.md +163 -0
- data/doc/context/SESSION-HANDOFF.md +69 -0
- data/doc/context/ai-engine-usage-trends-2026.md +80 -0
- data/doc/context/plan-pluggable-engines.md +590 -0
- data/doc/decisions/001-flog-deferred.md +32 -0
- data/doc/decisions/002-path-resolution-strategy.md +158 -0
- data/doc/decisions/003-ui-adapter-selection.md +193 -0
- data/doc/decisions/004-design-document-validation.md +179 -0
- data/doc/decisions/005-package-splitting-strategy.md +200 -0
- data/doc/decisions/006-multi-engine-architecture.md +147 -0
- data/doc/decisions/007-engine-agnostic-pivot.md +219 -0
- data/doc/decisions/008-ci-bundler-strategy.md +129 -0
- data/doc/decisions/009-core-only-v1-release.md +60 -0
- data/doc/decisions/010-engine-debian-packaging.md +66 -0
- data/doc/decisions/011-context-aware-cli.md +71 -0
- data/doc/dependency_decisions.yml +247 -0
- data/doc/issues/001-wrapper-missing-environment-variables.md +197 -0
- data/doc/issues/002-embedded-ruby-wrong-prefix.md +217 -0
- data/doc/issues/003-smoke-test-false-positive.md +127 -0
- data/doc/issues/004-market-research-design-updates.md +109 -0
- data/doc/issues/005-compile-scope-coexistence.md +161 -0
- data/doc/locales/.gitkeep +0 -0
- data/doc/man/rai.1.ronn +505 -0
- data/doc/operations/packaging.md +133 -0
- data/doc/operations/rosett-ai-release.md +65 -0
- data/doc/reference/error-catalog.md +107 -0
- data/doc/reference/rosett-ai-technical-reference.pdf +0 -0
- data/doc/reference/src/Pictures/cover.jpg +0 -0
- data/doc/reference/src/Pictures/head1.jpg +0 -0
- data/doc/reference/src/Pictures/head2.jpg +0 -0
- data/doc/reference/src/Pictures/head3.jpg +0 -0
- data/doc/reference/src/Pictures/head4.jpg +0 -0
- data/doc/reference/src/Pictures/head5.jpg +0 -0
- data/doc/reference/src/Pictures/head6.jpg +0 -0
- data/doc/reference/src/Pictures/head7.jpg +0 -0
- data/doc/reference/src/Pictures/head8.jpg +0 -0
- data/doc/reference/src/StyleInd.ist +4 -0
- data/doc/reference/src/bibliography.bib +79 -0
- data/doc/reference/src/main.tex +1288 -0
- data/doc/reference/src/structure.tex +303 -0
- data/doc/rosett-ai-bookmarks.html +301 -0
- data/kitchen.yml +46 -0
- data/lib/rosett_ai/adopter/executor_resolver.rb +77 -0
- data/lib/rosett_ai/adopter/local_analysis_collector.rb +154 -0
- data/lib/rosett_ai/adopter/rule_adopter.rb +254 -0
- data/lib/rosett_ai/ai_config/config_compiler.rb +111 -0
- data/lib/rosett_ai/ai_config/context_window.rb +55 -0
- data/lib/rosett_ai/ai_config/cost_controls.rb +44 -0
- data/lib/rosett_ai/ai_config/fallback_chain.rb +64 -0
- data/lib/rosett_ai/ai_config/model_router.rb +121 -0
- data/lib/rosett_ai/ai_config/validator.rb +45 -0
- data/lib/rosett_ai/authorship/attribution_compiler.rb +99 -0
- data/lib/rosett_ai/authorship/disclosure_policy.rb +81 -0
- data/lib/rosett_ai/authorship/review_validator.rb +39 -0
- data/lib/rosett_ai/authorship/trailer_generator.rb +88 -0
- data/lib/rosett_ai/backup/compressor.rb +180 -0
- data/lib/rosett_ai/backup/destination.rb +91 -0
- data/lib/rosett_ai/behaviour/manager.rb +156 -0
- data/lib/rosett_ai/compiler/backend.rb +86 -0
- data/lib/rosett_ai/compiler/backends/agents_md_backend.rb +80 -0
- data/lib/rosett_ai/compiler/backends/claude_backend.rb +88 -0
- data/lib/rosett_ai/compiler/backends/generic_backend.rb +15 -0
- data/lib/rosett_ai/compiler/behaviour_compiler.rb +40 -0
- data/lib/rosett_ai/compiler/capability_checker.rb +104 -0
- data/lib/rosett_ai/compiler/compilation_pipeline.rb +361 -0
- data/lib/rosett_ai/compiler/compiled_output.rb +39 -0
- data/lib/rosett_ai/compiler/locale_compiler.rb +250 -0
- data/lib/rosett_ai/compiler/target_profile.rb +112 -0
- data/lib/rosett_ai/completion/generator.rb +101 -0
- data/lib/rosett_ai/completion/shells/bash_generator.rb +126 -0
- data/lib/rosett_ai/completion/shells/fish_generator.rb +78 -0
- data/lib/rosett_ai/completion/shells/zsh_generator.rb +126 -0
- data/lib/rosett_ai/comply/checkers/cra_checker.rb +102 -0
- data/lib/rosett_ai/comply/checkers/license_checker.rb +85 -0
- data/lib/rosett_ai/comply/checkers/spdx_header_checker.rb +98 -0
- data/lib/rosett_ai/comply/reporter.rb +113 -0
- data/lib/rosett_ai/comply/runner.rb +50 -0
- data/lib/rosett_ai/composition/circular_dependency_detector.rb +56 -0
- data/lib/rosett_ai/composition/composer.rb +158 -0
- data/lib/rosett_ai/composition/composition_result.rb +64 -0
- data/lib/rosett_ai/composition/conflict_detector.rb +53 -0
- data/lib/rosett_ai/composition/lockfile.rb +103 -0
- data/lib/rosett_ai/composition/merge_strategy.rb +131 -0
- data/lib/rosett_ai/composition/priority_sorter.rb +29 -0
- data/lib/rosett_ai/composition/scope_resolver.rb +55 -0
- data/lib/rosett_ai/config/compile_result.rb +37 -0
- data/lib/rosett_ai/config/compiler.rb +13 -0
- data/lib/rosett_ai/config/domain_transformer.rb +13 -0
- data/lib/rosett_ai/config/key_map.rb +13 -0
- data/lib/rosett_ai/config/masking_secret_resolver.rb +40 -0
- data/lib/rosett_ai/config/scope_router.rb +13 -0
- data/lib/rosett_ai/config/secret_resolver.rb +125 -0
- data/lib/rosett_ai/configuration.rb +119 -0
- data/lib/rosett_ai/content/content_client.rb +60 -0
- data/lib/rosett_ai/content/pack_installer.rb +117 -0
- data/lib/rosett_ai/content/pack_manifest.rb +50 -0
- data/lib/rosett_ai/content/pack_registry.rb +68 -0
- data/lib/rosett_ai/content_packs/manager.rb +50 -0
- data/lib/rosett_ai/dbus/compositor_detector.rb +77 -0
- data/lib/rosett_ai/dbus/focus_adapters/base.rb +59 -0
- data/lib/rosett_ai/dbus/focus_adapters/gnome_adapter.rb +172 -0
- data/lib/rosett_ai/dbus/focus_adapters/hyprland_adapter.rb +77 -0
- data/lib/rosett_ai/dbus/focus_adapters/i3_adapter.rb +65 -0
- data/lib/rosett_ai/dbus/focus_adapters/kwin_adapter.rb +103 -0
- data/lib/rosett_ai/dbus/focus_adapters/x11_adapter.rb +105 -0
- data/lib/rosett_ai/dbus/focus_monitor_interface.rb +103 -0
- data/lib/rosett_ai/dbus/manager_interface.rb +213 -0
- data/lib/rosett_ai/dbus/plugin_manager_interface.rb +169 -0
- data/lib/rosett_ai/dbus/rate_limiter.rb +89 -0
- data/lib/rosett_ai/dbus/service.rb +121 -0
- data/lib/rosett_ai/dbus/status_notifier_interface.rb +79 -0
- data/lib/rosett_ai/deprecation.rb +79 -0
- data/lib/rosett_ai/desktop/dbus_client.rb +259 -0
- data/lib/rosett_ai/desktop/gtk4_app.rb +371 -0
- data/lib/rosett_ai/desktop/gtk4_preferences.rb +331 -0
- data/lib/rosett_ai/desktop/gui_logger.rb +236 -0
- data/lib/rosett_ai/doctor/check.rb +92 -0
- data/lib/rosett_ai/doctor/checks/cache_health_check.rb +50 -0
- data/lib/rosett_ai/doctor/checks/dbus_availability_check.rb +39 -0
- data/lib/rosett_ai/doctor/checks/engine_detection_check.rb +46 -0
- data/lib/rosett_ai/doctor/checks/file_permission_check.rb +44 -0
- data/lib/rosett_ai/doctor/checks/gem_dependency_check.rb +55 -0
- data/lib/rosett_ai/doctor/checks/ruby_version_check.rb +50 -0
- data/lib/rosett_ai/doctor/checks/stale_config_nncc_check.rb +57 -0
- data/lib/rosett_ai/doctor/checks/stale_home_nncc_check.rb +59 -0
- data/lib/rosett_ai/doctor.rb +81 -0
- data/lib/rosett_ai/documentation/reference_compiler.rb +122 -0
- data/lib/rosett_ai/documentation/translator.rb +62 -0
- data/lib/rosett_ai/engines/base_config_compiler.rb +203 -0
- data/lib/rosett_ai/engines/detector.rb +63 -0
- data/lib/rosett_ai/engines/registry.rb +50 -0
- data/lib/rosett_ai/error_handler.rb +139 -0
- data/lib/rosett_ai/exit_codes.rb +76 -0
- data/lib/rosett_ai/feature_flags.rb +102 -0
- data/lib/rosett_ai/formatting.rb +33 -0
- data/lib/rosett_ai/gem_consistency_checker.rb +199 -0
- data/lib/rosett_ai/git_hooks/chain_detector.rb +86 -0
- data/lib/rosett_ai/git_hooks/installer.rb +175 -0
- data/lib/rosett_ai/git_hooks/script_generator.rb +125 -0
- data/lib/rosett_ai/gitlab/validators/supplementary_gitlab_ci_yaml_validator.rb +79 -0
- data/lib/rosett_ai/i18n/locale_resolver.rb +46 -0
- data/lib/rosett_ai/i18n/utf8_checker.rb +32 -0
- data/lib/rosett_ai/init/config_file_writer.rb +24 -0
- data/lib/rosett_ai/init/directory_builder.rb +38 -0
- data/lib/rosett_ai/init/file_copier.rb +95 -0
- data/lib/rosett_ai/init/global_initializer.rb +28 -0
- data/lib/rosett_ai/init/local_initializer.rb +27 -0
- data/lib/rosett_ai/init/mcp_registrar.rb +109 -0
- data/lib/rosett_ai/init/project_initializer.rb +38 -0
- data/lib/rosett_ai/licensing/license_key.rb +139 -0
- data/lib/rosett_ai/licensing/license_store.rb +64 -0
- data/lib/rosett_ai/licensing/license_validator.rb +60 -0
- data/lib/rosett_ai/licensing/tier.rb +42 -0
- data/lib/rosett_ai/mcp/admin/auditor.rb +88 -0
- data/lib/rosett_ai/mcp/admin/health_checker.rb +81 -0
- data/lib/rosett_ai/mcp/admin/registry.rb +100 -0
- data/lib/rosett_ai/mcp/admin/schema_validator.rb +63 -0
- data/lib/rosett_ai/mcp/enforcement/.gitkeep +0 -0
- data/lib/rosett_ai/mcp/enforcement/hook_generator.rb +197 -0
- data/lib/rosett_ai/mcp/enforcement/validator.rb +215 -0
- data/lib/rosett_ai/mcp/governance.rb +160 -0
- data/lib/rosett_ai/mcp/http_security_config.rb +158 -0
- data/lib/rosett_ai/mcp/instructions.rb +266 -0
- data/lib/rosett_ai/mcp/key_hasher.rb +66 -0
- data/lib/rosett_ai/mcp/keyfile.rb +221 -0
- data/lib/rosett_ai/mcp/middleware/authentication.rb +146 -0
- data/lib/rosett_ai/mcp/middleware/content_type.rb +56 -0
- data/lib/rosett_ai/mcp/middleware/cors.rb +83 -0
- data/lib/rosett_ai/mcp/middleware/origin_validation.rb +73 -0
- data/lib/rosett_ai/mcp/middleware/rate_limit.rb +106 -0
- data/lib/rosett_ai/mcp/middleware/request_size.rb +51 -0
- data/lib/rosett_ai/mcp/plugins.rb +143 -0
- data/lib/rosett_ai/mcp/prompts/compilation_prompt.rb +40 -0
- data/lib/rosett_ai/mcp/prompts/compliance_prompt.rb +41 -0
- data/lib/rosett_ai/mcp/prompts/diagnostics_prompt.rb +41 -0
- data/lib/rosett_ai/mcp/prompts/validation_prompt.rb +41 -0
- data/lib/rosett_ai/mcp/resources/behaviour_resource.rb +127 -0
- data/lib/rosett_ai/mcp/resources/config_resource.rb +72 -0
- data/lib/rosett_ai/mcp/resources/design_resource.rb +58 -0
- data/lib/rosett_ai/mcp/resources/hooks_resource.rb +74 -0
- data/lib/rosett_ai/mcp/resources/provenance_resource.rb +51 -0
- data/lib/rosett_ai/mcp/resources/rules_resource.rb +60 -0
- data/lib/rosett_ai/mcp/resources/schema_resource.rb +72 -0
- data/lib/rosett_ai/mcp/response_helper.rb +46 -0
- data/lib/rosett_ai/mcp/security_logger.rb +60 -0
- data/lib/rosett_ai/mcp/server.rb +212 -0
- data/lib/rosett_ai/mcp/settings/server_installer.rb +112 -0
- data/lib/rosett_ai/mcp/settings/trust_manager.rb +142 -0
- data/lib/rosett_ai/mcp/tools/adopt_tool.rb +70 -0
- data/lib/rosett_ai/mcp/tools/backup_tool.rb +64 -0
- data/lib/rosett_ai/mcp/tools/behaviour_display_tool.rb +72 -0
- data/lib/rosett_ai/mcp/tools/behaviour_list_tool.rb +56 -0
- data/lib/rosett_ai/mcp/tools/behaviour_manage_tool.rb +114 -0
- data/lib/rosett_ai/mcp/tools/behaviour_show_tool.rb +62 -0
- data/lib/rosett_ai/mcp/tools/compile_status_tool.rb +122 -0
- data/lib/rosett_ai/mcp/tools/compile_tool.rb +191 -0
- data/lib/rosett_ai/mcp/tools/comply_tool.rb +79 -0
- data/lib/rosett_ai/mcp/tools/config_compile_tool.rb +71 -0
- data/lib/rosett_ai/mcp/tools/config_status_tool.rb +79 -0
- data/lib/rosett_ai/mcp/tools/content_tool.rb +78 -0
- data/lib/rosett_ai/mcp/tools/context_query_tool.rb +156 -0
- data/lib/rosett_ai/mcp/tools/design_list_tool.rb +57 -0
- data/lib/rosett_ai/mcp/tools/design_show_tool.rb +69 -0
- data/lib/rosett_ai/mcp/tools/doctor_tool.rb +62 -0
- data/lib/rosett_ai/mcp/tools/documentation_status_tool.rb +45 -0
- data/lib/rosett_ai/mcp/tools/engines_tool.rb +84 -0
- data/lib/rosett_ai/mcp/tools/hook_install_tool.rb +190 -0
- data/lib/rosett_ai/mcp/tools/hook_preview_tool.rb +173 -0
- data/lib/rosett_ai/mcp/tools/hooks_status_tool.rb +84 -0
- data/lib/rosett_ai/mcp/tools/init_tool.rb +87 -0
- data/lib/rosett_ai/mcp/tools/license_status_tool.rb +44 -0
- data/lib/rosett_ai/mcp/tools/project_tool.rb +117 -0
- data/lib/rosett_ai/mcp/tools/provenance_tool.rb +97 -0
- data/lib/rosett_ai/mcp/tools/provenance_write_tool.rb +40 -0
- data/lib/rosett_ai/mcp/tools/retrofit_tool.rb +81 -0
- data/lib/rosett_ai/mcp/tools/rule_search_tool.rb +163 -0
- data/lib/rosett_ai/mcp/tools/schema_get_tool.rb +94 -0
- data/lib/rosett_ai/mcp/tools/tooling_tool.rb +86 -0
- data/lib/rosett_ai/mcp/tools/validate_tool.rb +105 -0
- data/lib/rosett_ai/mcp/tools/workflow_execute_tool.rb +74 -0
- data/lib/rosett_ai/mcp/tools/workflow_tool.rb +78 -0
- data/lib/rosett_ai/migration/detector.rb +117 -0
- data/lib/rosett_ai/migration/nncc_config_migrator.rb +94 -0
- data/lib/rosett_ai/migration/nncc_project_migrator.rb +90 -0
- data/lib/rosett_ai/migration/xdg_migrator.rb +123 -0
- data/lib/rosett_ai/package_manager/apt.rb +108 -0
- data/lib/rosett_ai/package_manager/base.rb +68 -0
- data/lib/rosett_ai/package_manager/gem_backend.rb +90 -0
- data/lib/rosett_ai/packaging/variant_config.rb +92 -0
- data/lib/rosett_ai/path_resolver.rb +115 -0
- data/lib/rosett_ai/plugins/contract.rb +43 -0
- data/lib/rosett_ai/plugins/engine_contract.rb +60 -0
- data/lib/rosett_ai/plugins/gui_contract.rb +74 -0
- data/lib/rosett_ai/plugins/mcp_contract.rb +48 -0
- data/lib/rosett_ai/plugins/registry.rb +150 -0
- data/lib/rosett_ai/policy/auditor.rb +41 -0
- data/lib/rosett_ai/policy/deny_list.rb +71 -0
- data/lib/rosett_ai/policy/opt_out_scanner.rb +37 -0
- data/lib/rosett_ai/policy/policy_compiler.rb +84 -0
- data/lib/rosett_ai/policy/protected_files.rb +47 -0
- data/lib/rosett_ai/policy/tier_hierarchy.rb +48 -0
- data/lib/rosett_ai/policy/validator.rb +35 -0
- data/lib/rosett_ai/profiler.rb +79 -0
- data/lib/rosett_ai/project/drift_detector.rb +126 -0
- data/lib/rosett_ai/project/manager.rb +115 -0
- data/lib/rosett_ai/project/sync_manager.rb +138 -0
- data/lib/rosett_ai/project/template_applier.rb +105 -0
- data/lib/rosett_ai/project_context.rb +82 -0
- data/lib/rosett_ai/provenance/entry.rb +63 -0
- data/lib/rosett_ai/provenance/file_source.rb +32 -0
- data/lib/rosett_ai/provenance/source.rb +62 -0
- data/lib/rosett_ai/provenance/store.rb +153 -0
- data/lib/rosett_ai/provenance/tracker.rb +62 -0
- data/lib/rosett_ai/provenance/trailer_generator.rb +43 -0
- data/lib/rosett_ai/provenance/validator.rb +45 -0
- data/lib/rosett_ai/quorum/collector.rb +59 -0
- data/lib/rosett_ai/quorum/comparator.rb +81 -0
- data/lib/rosett_ai/quorum/dispatcher.rb +57 -0
- data/lib/rosett_ai/quorum/strategies/adopt.rb +56 -0
- data/lib/rosett_ai/rai_config.rb +107 -0
- data/lib/rosett_ai/retrofit/base_parser.rb +66 -0
- data/lib/rosett_ai/retrofit/engine.rb +171 -0
- data/lib/rosett_ai/retrofit/parsers/agents_md_parser.rb +50 -0
- data/lib/rosett_ai/retrofit/parsers/claude_parser.rb +69 -0
- data/lib/rosett_ai/retrofit/parsers/cursor_parser.rb +82 -0
- data/lib/rosett_ai/retrofit/round_trip_validator.rb +65 -0
- data/lib/rosett_ai/retrofit/scanner.rb +47 -0
- data/lib/rosett_ai/retrofit/secret_detector.rb +87 -0
- data/lib/rosett_ai/secrets_resolver.rb +71 -0
- data/lib/rosett_ai/smart_feedback/suggester.rb +83 -0
- data/lib/rosett_ai/smart_feedback/thor_middleware.rb +84 -0
- data/lib/rosett_ai/structured_logger.rb +110 -0
- data/lib/rosett_ai/telemetry/json_lines_writer.rb +50 -0
- data/lib/rosett_ai/telemetry/log_rotator.rb +67 -0
- data/lib/rosett_ai/telemetry/provider.rb +26 -0
- data/lib/rosett_ai/telemetry/reporter.rb +144 -0
- data/lib/rosett_ai/telemetry.rb +47 -0
- data/lib/rosett_ai/text_sanitizer.rb +62 -0
- data/lib/rosett_ai/thor/cli.rb +269 -0
- data/lib/rosett_ai/thor/tasks/adopt.rb +250 -0
- data/lib/rosett_ai/thor/tasks/backup.rb +420 -0
- data/lib/rosett_ai/thor/tasks/behaviour.rb +474 -0
- data/lib/rosett_ai/thor/tasks/build.rb +1162 -0
- data/lib/rosett_ai/thor/tasks/compile.rb +415 -0
- data/lib/rosett_ai/thor/tasks/completion.rb +123 -0
- data/lib/rosett_ai/thor/tasks/comply.rb +82 -0
- data/lib/rosett_ai/thor/tasks/config.rb +265 -0
- data/lib/rosett_ai/thor/tasks/content.rb +193 -0
- data/lib/rosett_ai/thor/tasks/dbus.rb +321 -0
- data/lib/rosett_ai/thor/tasks/design.rb +258 -0
- data/lib/rosett_ai/thor/tasks/desktop.rb +129 -0
- data/lib/rosett_ai/thor/tasks/doctor.rb +127 -0
- data/lib/rosett_ai/thor/tasks/documentation.rb +321 -0
- data/lib/rosett_ai/thor/tasks/engines.rb +167 -0
- data/lib/rosett_ai/thor/tasks/hooks.rb +219 -0
- data/lib/rosett_ai/thor/tasks/init.rb +259 -0
- data/lib/rosett_ai/thor/tasks/license.rb +120 -0
- data/lib/rosett_ai/thor/tasks/mcp.rb +535 -0
- data/lib/rosett_ai/thor/tasks/migrate.rb +121 -0
- data/lib/rosett_ai/thor/tasks/plugins.rb +157 -0
- data/lib/rosett_ai/thor/tasks/project.rb +260 -0
- data/lib/rosett_ai/thor/tasks/provenance.rb +195 -0
- data/lib/rosett_ai/thor/tasks/release.rb +314 -0
- data/lib/rosett_ai/thor/tasks/retrofit.rb +90 -0
- data/lib/rosett_ai/thor/tasks/tooling.rb +308 -0
- data/lib/rosett_ai/thor/tasks/validate.rb +108 -0
- data/lib/rosett_ai/thor/tasks/workflow.rb +196 -0
- data/lib/rosett_ai/tooling/ci_yaml_validator.rb +37 -0
- data/lib/rosett_ai/tooling/version_checker.rb +35 -0
- data/lib/rosett_ai/ui/accessible_tui.rb +61 -0
- data/lib/rosett_ai/ui/base.rb +46 -0
- data/lib/rosett_ai/ui/gtk4.rb +98 -0
- data/lib/rosett_ai/ui/kde.rb +40 -0
- data/lib/rosett_ai/ui/qt6.rb +40 -0
- data/lib/rosett_ai/ui/registry.rb +60 -0
- data/lib/rosett_ai/ui/tty_helper.rb +74 -0
- data/lib/rosett_ai/ui/tui.rb +59 -0
- data/lib/rosett_ai/validators/behaviour_validator.rb +20 -0
- data/lib/rosett_ai/validators/design_validator.rb +17 -0
- data/lib/rosett_ai/validators/schema_validator.rb +84 -0
- data/lib/rosett_ai/validators/tooling_validator.rb +17 -0
- data/lib/rosett_ai/version.rb +8 -0
- data/lib/rosett_ai/version_consistency_checker.rb +129 -0
- data/lib/rosett_ai/workflow/audit_log.rb +86 -0
- data/lib/rosett_ai/workflow/engine.rb +142 -0
- data/lib/rosett_ai/workflow/manager.rb +82 -0
- data/lib/rosett_ai/workflow/schema_validator.rb +71 -0
- data/lib/rosett_ai/workflow/step_runner.rb +61 -0
- data/lib/rosett_ai/workflow/steps/prompt_step.rb +62 -0
- data/lib/rosett_ai/workflow/steps/rai_step.rb +74 -0
- data/lib/rosett_ai/workflow/steps/shell_step.rb +53 -0
- data/lib/rosett_ai/yaml_loader.rb +78 -0
- data/lib/rosett_ai.rb +221 -0
- data/lib/rubocop/cop/rosett_ai/shell_interpolation.rb +54 -0
- data/lib/rubocop/cop/rosett_ai/unsafe_const_get.rb +60 -0
- data/lib/rubocop/cop/rosett_ai/unsafe_send.rb +50 -0
- data/lib/rubocop/cop/rosett_ai/unsafe_yaml_load.rb +40 -0
- data/lib/rubocop/rosett_ai.rb +9 -0
- data/lib/scripts/generated/docker_hub_tags.rb +126 -0
- data/locales/.gitkeep +0 -0
- data/locales/ar.yml +579 -0
- data/locales/en.yml +571 -0
- data/locales/fr.yml +567 -0
- data/packaging/build-engine-deb.sh +81 -0
- data/packaging/scripts/postinst +17 -0
- data/packaging/scripts/postrm +19 -0
- data/packaging/scripts/prerm +10 -0
- data/packaging/wrapper.sh.template +38 -0
- data/rosett-ai.gemspec +63 -0
- data/rules/.gitkeep +0 -0
- data/scripts/publish/pulp_upload.sh +123 -0
- data/settings.json +29 -0
- data/share/applications/be.neatnerds.rosettai.desktop +29 -0
- data/share/dbus-1/interfaces/be.neatnerds.rosettai.xml +103 -0
- data/share/dbus-1/services/be.neatnerds.rosettai.service +3 -0
- data/share/templates/behaviour/criticalthinking.yml +69 -0
- 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
|