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,100 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# SPDX-License-Identifier: GPL-3.0-only
|
|
4
|
+
# Copyright (C) 2026 Hugo Antonio Sepulveda Manriquez / NeatNerds
|
|
5
|
+
|
|
6
|
+
module RosettAi
|
|
7
|
+
module Mcp
|
|
8
|
+
module Admin
|
|
9
|
+
# Registry of configured MCP servers.
|
|
10
|
+
#
|
|
11
|
+
# Manages discovery, storage, and querying of MCP server
|
|
12
|
+
# configurations from XDG-compliant paths.
|
|
13
|
+
#
|
|
14
|
+
# @author hugo
|
|
15
|
+
# @author claude
|
|
16
|
+
class Registry
|
|
17
|
+
CONFIG_DIR = '.config/rosett-ai/mcp/servers'
|
|
18
|
+
|
|
19
|
+
# @param config_dir [Pathname, nil] override config directory
|
|
20
|
+
def initialize(config_dir: nil)
|
|
21
|
+
@config_dir = config_dir || Pathname.new(Dir.home).join(CONFIG_DIR)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Lists all registered MCP servers.
|
|
25
|
+
#
|
|
26
|
+
# @return [Array<Hash>] server entries with :name, :transport, :command/:url
|
|
27
|
+
def list
|
|
28
|
+
return [] unless @config_dir.directory?
|
|
29
|
+
|
|
30
|
+
@config_dir.glob('*.yml').filter_map { |path| load_server(path) }
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Finds a server by name.
|
|
34
|
+
#
|
|
35
|
+
# @param name [String] server name
|
|
36
|
+
# @return [Hash, nil] server entry or nil
|
|
37
|
+
def find(name)
|
|
38
|
+
list.find { |s| s[:name] == name }
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Adds a server configuration.
|
|
42
|
+
#
|
|
43
|
+
# @param config [Hash] server configuration
|
|
44
|
+
# @return [Pathname] path to the written file
|
|
45
|
+
def add(config)
|
|
46
|
+
FileUtils.mkdir_p(@config_dir)
|
|
47
|
+
name = config.fetch('name') { config.fetch(:name) }
|
|
48
|
+
path = @config_dir.join("#{name}.yml")
|
|
49
|
+
|
|
50
|
+
File.write(path, YAML.dump(stringify_keys(config)))
|
|
51
|
+
path
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Removes a server configuration.
|
|
55
|
+
#
|
|
56
|
+
# @param name [String] server name
|
|
57
|
+
# @return [Boolean] true if removed
|
|
58
|
+
def remove(name) # rubocop:disable Naming/PredicateMethod -- destructive action, not a predicate
|
|
59
|
+
path = @config_dir.join("#{name}.yml")
|
|
60
|
+
return false unless path.exist?
|
|
61
|
+
|
|
62
|
+
File.delete(path)
|
|
63
|
+
true
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Returns all server config file paths.
|
|
67
|
+
#
|
|
68
|
+
# @return [Array<Pathname>]
|
|
69
|
+
def config_paths
|
|
70
|
+
return [] unless @config_dir.directory?
|
|
71
|
+
|
|
72
|
+
@config_dir.glob('*.yml')
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
private
|
|
76
|
+
|
|
77
|
+
def load_server(path)
|
|
78
|
+
data = YAML.safe_load_file(path, permitted_classes: [Symbol])
|
|
79
|
+
return nil unless data.is_a?(Hash)
|
|
80
|
+
|
|
81
|
+
{
|
|
82
|
+
name: data['name'],
|
|
83
|
+
transport: data['transport'] || 'stdio',
|
|
84
|
+
command: data['command'],
|
|
85
|
+
url: data['url'],
|
|
86
|
+
args: data['args'],
|
|
87
|
+
env: data['env'],
|
|
88
|
+
source_file: path.to_s
|
|
89
|
+
}
|
|
90
|
+
rescue Psych::SyntaxError
|
|
91
|
+
nil
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def stringify_keys(hash)
|
|
95
|
+
hash.transform_keys(&:to_s)
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# SPDX-License-Identifier: GPL-3.0-only
|
|
4
|
+
# Copyright (C) 2026 Hugo Antonio Sepulveda Manriquez / NeatNerds
|
|
5
|
+
|
|
6
|
+
require 'json_schemer'
|
|
7
|
+
|
|
8
|
+
module RosettAi
|
|
9
|
+
module Mcp
|
|
10
|
+
module Admin
|
|
11
|
+
# Validates MCP server configurations against JSON Schema.
|
|
12
|
+
#
|
|
13
|
+
# Uses conf/schemas/mcp_server_schema.json for validation.
|
|
14
|
+
#
|
|
15
|
+
# @author hugo
|
|
16
|
+
# @author claude
|
|
17
|
+
class SchemaValidator
|
|
18
|
+
# @param schema_path [Pathname, nil] override schema file path
|
|
19
|
+
def initialize(schema_path: nil)
|
|
20
|
+
@schema_path = schema_path || RosettAi.root.join('conf', 'schemas', 'mcp_server_schema.json')
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Validates a server configuration hash.
|
|
24
|
+
#
|
|
25
|
+
# @param server [Hash] server configuration
|
|
26
|
+
# @return [Hash] result with :valid and :errors keys
|
|
27
|
+
def validate(server)
|
|
28
|
+
schema = load_schema
|
|
29
|
+
return { valid: true, errors: [] } unless schema
|
|
30
|
+
|
|
31
|
+
data = normalize(server)
|
|
32
|
+
errors = schema.validate(data).map { |e| e['error'] }
|
|
33
|
+
|
|
34
|
+
{ valid: errors.empty?, errors: errors }
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Validates all servers in a registry.
|
|
38
|
+
#
|
|
39
|
+
# @param registry [Registry] server registry
|
|
40
|
+
# @return [Array<Hash>] validation results
|
|
41
|
+
def validate_all(registry)
|
|
42
|
+
registry.list.map do |server|
|
|
43
|
+
result = validate(server)
|
|
44
|
+
result.merge(name: server[:name])
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
private
|
|
49
|
+
|
|
50
|
+
def load_schema
|
|
51
|
+
return nil unless @schema_path.exist?
|
|
52
|
+
|
|
53
|
+
data = JSON.parse(File.read(@schema_path))
|
|
54
|
+
JSONSchemer.schema(data)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def normalize(server)
|
|
58
|
+
server.transform_keys(&:to_s).compact
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
File without changes
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# SPDX-License-Identifier: GPL-3.0-only
|
|
4
|
+
# Copyright (C) 2026 Hugo Antonio Sepulveda Manriquez / NeatNerds
|
|
5
|
+
|
|
6
|
+
require 'time'
|
|
7
|
+
|
|
8
|
+
module RosettAi
|
|
9
|
+
module Mcp
|
|
10
|
+
module Enforcement
|
|
11
|
+
# Generates self-contained Ruby PreToolUse hook scripts from validated
|
|
12
|
+
# enforcement rules.
|
|
13
|
+
#
|
|
14
|
+
# Generated scripts are standalone — they require only Ruby stdlib (json)
|
|
15
|
+
# and have zero runtime dependency on rosett-ai. Each script includes:
|
|
16
|
+
# - YARD documentation
|
|
17
|
+
# - Human-readable header with generation metadata
|
|
18
|
+
# - AI client instructions comment block
|
|
19
|
+
# - Traceability (source behaviour, rule ID, version, generator version)
|
|
20
|
+
# - Enforcement logic: regex match on tool input, block/warn/log action
|
|
21
|
+
#
|
|
22
|
+
# @example Generate a hook script for validated rules
|
|
23
|
+
# generator = HookGenerator.new
|
|
24
|
+
# script = generator.generate(validated_rules, behaviour_name: 'session_optimization',
|
|
25
|
+
# behaviour_version: '1.0.0')
|
|
26
|
+
#
|
|
27
|
+
# @author hugo
|
|
28
|
+
# @author claude
|
|
29
|
+
class HookGenerator
|
|
30
|
+
# Generates a complete PreToolUse hook script from validated rules.
|
|
31
|
+
#
|
|
32
|
+
# @param rules [Array<Hash>] validated rules from {Validator}, each with
|
|
33
|
+
# :rule_id, :behaviour, :pattern, :applies_to, :action, :message,
|
|
34
|
+
# :priority, :description
|
|
35
|
+
# @param behaviour_name [String] source behaviour name
|
|
36
|
+
# @param behaviour_version [String] source behaviour version
|
|
37
|
+
# @return [String] complete Ruby hook script
|
|
38
|
+
def generate(rules, behaviour_name:, behaviour_version:)
|
|
39
|
+
timestamp = Time.now.utc.iso8601
|
|
40
|
+
parts = []
|
|
41
|
+
parts << shebang_and_magic
|
|
42
|
+
parts << yard_documentation(rules)
|
|
43
|
+
parts << traceability_header(behaviour_name, behaviour_version, rules, timestamp)
|
|
44
|
+
parts << ai_client_instructions
|
|
45
|
+
parts << rule_constant(rules)
|
|
46
|
+
parts << enforcement_body
|
|
47
|
+
parts.join("\n")
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
private
|
|
51
|
+
|
|
52
|
+
# @return [String]
|
|
53
|
+
def shebang_and_magic
|
|
54
|
+
<<~RUBY
|
|
55
|
+
#!/usr/bin/env ruby
|
|
56
|
+
# frozen_string_literal: true
|
|
57
|
+
RUBY
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Generates YARD documentation block for the hook script.
|
|
61
|
+
#
|
|
62
|
+
# @param rules [Array<Hash>]
|
|
63
|
+
# @return [String]
|
|
64
|
+
def yard_documentation(rules)
|
|
65
|
+
rule_list = rules.map { |r| "# - #{r[:behaviour]}/#{r[:rule_id]} (P#{r[:priority]})" }
|
|
66
|
+
<<~RUBY
|
|
67
|
+
# @title Rosett-AI Enforcement Hook
|
|
68
|
+
# @description Auto-generated PreToolUse hook that enforces behaviour rules
|
|
69
|
+
# by matching tool input against regex patterns. Self-contained — no
|
|
70
|
+
# external gem dependencies.
|
|
71
|
+
#
|
|
72
|
+
# @note This file is auto-generated by rosett-ai. Do not edit manually.
|
|
73
|
+
# Re-generate with: raictl mcp tool rai_hook_install
|
|
74
|
+
#
|
|
75
|
+
# Enforced rules:
|
|
76
|
+
#{rule_list.join("\n")}
|
|
77
|
+
RUBY
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# Generates traceability header with generation metadata.
|
|
81
|
+
#
|
|
82
|
+
# @param behaviour_name [String]
|
|
83
|
+
# @param behaviour_version [String]
|
|
84
|
+
# @param rules [Array<Hash>]
|
|
85
|
+
# @param timestamp [String]
|
|
86
|
+
# @return [String]
|
|
87
|
+
def traceability_header(behaviour_name, behaviour_version, rules, timestamp)
|
|
88
|
+
<<~RUBY
|
|
89
|
+
# --- Traceability ---
|
|
90
|
+
# Source behaviour: #{behaviour_name}
|
|
91
|
+
# Behaviour version: #{behaviour_version}
|
|
92
|
+
# Generator: rosett-ai v#{RosettAi::VERSION}
|
|
93
|
+
# Generated at: #{timestamp}
|
|
94
|
+
# Rule count: #{rules.size}
|
|
95
|
+
# Rule IDs: #{rules.map { |r| r[:rule_id] }.join(', ')}
|
|
96
|
+
RUBY
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# @return [String]
|
|
100
|
+
def ai_client_instructions
|
|
101
|
+
<<~RUBY
|
|
102
|
+
# --- AI Client Instructions ---
|
|
103
|
+
# This hook is a Claude Code PreToolUse hook. It reads JSON from stdin
|
|
104
|
+
# describing the tool invocation, checks the input against enforcement
|
|
105
|
+
# rules, and exits with:
|
|
106
|
+
# exit 0 — tool call allowed (or warn/log mode)
|
|
107
|
+
# exit 2 — tool call blocked (block mode, non-zero = reject)
|
|
108
|
+
# Stderr output is shown to the user as hook feedback.
|
|
109
|
+
# Do NOT modify this file — re-generate via rai_hook_install.
|
|
110
|
+
RUBY
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# Generates the RULES constant with validated rule data.
|
|
114
|
+
#
|
|
115
|
+
# @param rules [Array<Hash>]
|
|
116
|
+
# @return [String]
|
|
117
|
+
def rule_constant(rules)
|
|
118
|
+
entries = rules.map { |r| format_rule_entry(r) }
|
|
119
|
+
<<~RUBY
|
|
120
|
+
RULES = [
|
|
121
|
+
#{entries.join(",\n")}
|
|
122
|
+
].freeze
|
|
123
|
+
RUBY
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# Formats a single rule entry for the RULES constant.
|
|
127
|
+
#
|
|
128
|
+
# @param rule [Hash]
|
|
129
|
+
# @return [String]
|
|
130
|
+
def format_rule_entry(rule)
|
|
131
|
+
desc = sanitize_string(rule[:description], 80)
|
|
132
|
+
msg = sanitize_string(rule[:message].to_s, 120)
|
|
133
|
+
tools = rule[:applies_to].map { |t| "'#{sanitize_string(t, 40)}'" }.join(', ')
|
|
134
|
+
behaviour = sanitize_string(rule[:behaviour], 100)
|
|
135
|
+
rule_id = sanitize_string(rule[:rule_id], 40)
|
|
136
|
+
[
|
|
137
|
+
" { behaviour: '#{behaviour}',",
|
|
138
|
+
" rule_id: '#{rule_id}',",
|
|
139
|
+
" priority: #{rule[:priority]},",
|
|
140
|
+
" pattern: #{format_regex(rule[:pattern])},",
|
|
141
|
+
" applies_to: [#{tools}],",
|
|
142
|
+
" action: :#{rule[:action]},",
|
|
143
|
+
" message: '#{msg}',",
|
|
144
|
+
" description: '#{desc}' }"
|
|
145
|
+
].join("\n ")
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# Formats a pattern string as a Regexp literal for the generated script.
|
|
149
|
+
#
|
|
150
|
+
# @param pattern [String]
|
|
151
|
+
# @return [String]
|
|
152
|
+
def format_regex(pattern)
|
|
153
|
+
"/#{pattern.gsub('/', '\\/')}/"
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
# Sanitizes a string for safe embedding in single-quoted Ruby strings.
|
|
157
|
+
# Backslashes must be escaped before quotes to avoid double-escaping.
|
|
158
|
+
#
|
|
159
|
+
# @param value [String]
|
|
160
|
+
# @param max_length [Integer]
|
|
161
|
+
# @return [String]
|
|
162
|
+
def sanitize_string(value, max_length)
|
|
163
|
+
value.to_s.tr("\n", ' ').strip[0, max_length].gsub('\\', '\\\\\\\\').gsub("'", "\\\\'")
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
# Generates the runtime enforcement logic body.
|
|
167
|
+
# Uses non-interpolating heredoc — this code runs at hook time, not generation time.
|
|
168
|
+
#
|
|
169
|
+
# @return [String]
|
|
170
|
+
def enforcement_body
|
|
171
|
+
<<~'RUBY'
|
|
172
|
+
require 'json'
|
|
173
|
+
|
|
174
|
+
input = JSON.parse($stdin.read)
|
|
175
|
+
tool_name = input['tool_name'].to_s
|
|
176
|
+
tool_input = input['tool_input'].to_s
|
|
177
|
+
|
|
178
|
+
violations = RULES.select do |rule|
|
|
179
|
+
rule[:applies_to].include?(tool_name) && rule[:pattern].match?(tool_input)
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
exit 0 if violations.empty?
|
|
183
|
+
|
|
184
|
+
violations.sort_by { |v| -v[:priority] }.each do |v|
|
|
185
|
+
$stderr.puts "[rai-enforce] #{v[:behaviour]}/#{v[:rule_id]} " \
|
|
186
|
+
"(P#{v[:priority]}, #{v[:action]}): #{v[:message].empty? ? v[:description] : v[:message]}"
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
# Block if any violation has action :block
|
|
190
|
+
blocked = violations.any? { |v| v[:action] == :block }
|
|
191
|
+
exit(blocked ? 2 : 0)
|
|
192
|
+
RUBY
|
|
193
|
+
end
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
end
|
|
197
|
+
end
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# SPDX-License-Identifier: GPL-3.0-only
|
|
4
|
+
# Copyright (C) 2026 Hugo Antonio Sepulveda Manriquez / NeatNerds
|
|
5
|
+
|
|
6
|
+
module RosettAi
|
|
7
|
+
module Mcp
|
|
8
|
+
module Enforcement
|
|
9
|
+
# Validates enforcement blocks in behaviour rules.
|
|
10
|
+
#
|
|
11
|
+
# Checks each rule's enforcement block for correctness:
|
|
12
|
+
# - pattern present and compilable as regex
|
|
13
|
+
# - pattern not degenerate (would match everything)
|
|
14
|
+
# - applies_to present and non-empty
|
|
15
|
+
# - action is a valid enum value
|
|
16
|
+
#
|
|
17
|
+
# Invalid enforceable rules are downgraded to advisory with warnings.
|
|
18
|
+
# Rules without enforcement blocks are skipped (backward compatible).
|
|
19
|
+
#
|
|
20
|
+
# @example
|
|
21
|
+
# validator = Validator.new
|
|
22
|
+
# result = validator.validate(behaviour_data)
|
|
23
|
+
# result[:valid] # => [{ rule_id: "rule_001", ... }]
|
|
24
|
+
# result[:downgraded] # => [{ rule_id: "rule_002", reason: "..." }]
|
|
25
|
+
# result[:skipped] # => [{ rule_id: "rule_003" }]
|
|
26
|
+
#
|
|
27
|
+
# @author hugo
|
|
28
|
+
# @author claude
|
|
29
|
+
class Validator
|
|
30
|
+
VALID_TYPES = ['enforceable', 'advisory', 'informational'].freeze
|
|
31
|
+
VALID_ACTIONS = ['block', 'warn', 'log'].freeze
|
|
32
|
+
|
|
33
|
+
# Patterns that match everything or nearly everything — unsafe for hooks.
|
|
34
|
+
DEGENERATE_PATTERNS = [
|
|
35
|
+
/\A\.\*\z/, # .*
|
|
36
|
+
/\A\.\+\z/, # .+
|
|
37
|
+
/\A\\s\*\z/, # \s*
|
|
38
|
+
/\A\\S\*\z/, # \S*
|
|
39
|
+
/\A\\s\+\z/, # \s+
|
|
40
|
+
/\A\\S\+\z/, # \S+
|
|
41
|
+
/\A\.\*\?\z/, # .*?
|
|
42
|
+
/\A\.\+\?\z/, # .+?
|
|
43
|
+
/\A\(\.\*\)\z/, # (.*)
|
|
44
|
+
/\A\(\.\+\)\z/ # (.+)
|
|
45
|
+
].freeze
|
|
46
|
+
|
|
47
|
+
# @!attribute [r] behaviour
|
|
48
|
+
# @return [String] behaviour name for error messages
|
|
49
|
+
# @!attribute [r] rule_id
|
|
50
|
+
# @return [String] current rule ID for error messages
|
|
51
|
+
RuleContext = Struct.new(:behaviour, :rule_id)
|
|
52
|
+
|
|
53
|
+
# Validates all enforcement blocks in a behaviour.
|
|
54
|
+
#
|
|
55
|
+
# @param behaviour [Hash] parsed behaviour YAML data
|
|
56
|
+
# @return [Hash] result with :valid, :downgraded, :skipped, :errors keys
|
|
57
|
+
def validate(behaviour)
|
|
58
|
+
@result = { valid: [], downgraded: [], skipped: [], errors: [] }
|
|
59
|
+
rules = behaviour['rules']
|
|
60
|
+
return @result unless rules.is_a?(Array)
|
|
61
|
+
|
|
62
|
+
behaviour_name = behaviour['name'].to_s
|
|
63
|
+
rules.each { |rule| validate_rule(rule, behaviour_name) }
|
|
64
|
+
|
|
65
|
+
@result
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
private
|
|
69
|
+
|
|
70
|
+
# Validates a single rule's enforcement block.
|
|
71
|
+
#
|
|
72
|
+
# @param rule [Hash] rule data from behaviour YAML
|
|
73
|
+
# @param behaviour_name [String] parent behaviour name
|
|
74
|
+
def validate_rule(rule, behaviour_name)
|
|
75
|
+
return unless rule.is_a?(Hash)
|
|
76
|
+
|
|
77
|
+
ctx = RuleContext.new(behaviour: behaviour_name, rule_id: rule['id'].to_s)
|
|
78
|
+
enforcement = rule['enforcement']
|
|
79
|
+
|
|
80
|
+
unless enforcement.is_a?(Hash)
|
|
81
|
+
@result[:skipped] << { rule_id: ctx.rule_id, behaviour: ctx.behaviour }
|
|
82
|
+
return
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
classify_enforcement(rule, enforcement, ctx)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Classifies a rule that has an enforcement block.
|
|
89
|
+
#
|
|
90
|
+
# @param rule [Hash]
|
|
91
|
+
# @param enforcement [Hash]
|
|
92
|
+
# @param ctx [RuleContext]
|
|
93
|
+
def classify_enforcement(rule, enforcement, ctx)
|
|
94
|
+
type = enforcement['type'].to_s
|
|
95
|
+
unless VALID_TYPES.include?(type)
|
|
96
|
+
@result[:errors] << { rule_id: ctx.rule_id, behaviour: ctx.behaviour,
|
|
97
|
+
reason: "invalid enforcement type '#{type}' " \
|
|
98
|
+
"(must be one of: #{VALID_TYPES.join(', ')})" }
|
|
99
|
+
return
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
return record_non_enforceable(rule, ctx, type) unless type == 'enforceable'
|
|
103
|
+
|
|
104
|
+
validate_enforceable(rule, enforcement, ctx)
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# Validates and classifies an enforceable rule.
|
|
108
|
+
#
|
|
109
|
+
# @param rule [Hash]
|
|
110
|
+
# @param enforcement [Hash]
|
|
111
|
+
# @param ctx [RuleContext]
|
|
112
|
+
def validate_enforceable(rule, enforcement, ctx)
|
|
113
|
+
warnings = collect_warnings(enforcement, ctx)
|
|
114
|
+
|
|
115
|
+
if warnings.empty?
|
|
116
|
+
@result[:valid] << build_valid_entry(rule, enforcement, ctx)
|
|
117
|
+
else
|
|
118
|
+
@result[:downgraded] << { rule_id: ctx.rule_id, behaviour: ctx.behaviour,
|
|
119
|
+
original_type: 'enforceable', downgraded_to: 'advisory',
|
|
120
|
+
warnings: warnings }
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# Records advisory/informational rules (no field validation needed).
|
|
125
|
+
#
|
|
126
|
+
# @param rule [Hash]
|
|
127
|
+
# @param ctx [RuleContext]
|
|
128
|
+
# @param type [String]
|
|
129
|
+
def record_non_enforceable(rule, ctx, type)
|
|
130
|
+
@result[:valid] << { rule_id: rule['id'].to_s, behaviour: ctx.behaviour, type: type }
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# Collects all warnings for an enforceable enforcement block.
|
|
134
|
+
#
|
|
135
|
+
# @param enforcement [Hash] the enforcement block
|
|
136
|
+
# @param ctx [RuleContext]
|
|
137
|
+
# @return [Array<String>]
|
|
138
|
+
def collect_warnings(enforcement, ctx)
|
|
139
|
+
prefix = "#{ctx.behaviour}/#{ctx.rule_id}"
|
|
140
|
+
[].concat(check_pattern(enforcement['pattern'], prefix))
|
|
141
|
+
.concat(check_applies_to(enforcement['applies_to'], prefix))
|
|
142
|
+
.concat(check_action(enforcement['action'], prefix))
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
# Validates the pattern field.
|
|
146
|
+
#
|
|
147
|
+
# @param pattern [String, nil]
|
|
148
|
+
# @param prefix [String] error message prefix
|
|
149
|
+
# @return [Array<String>]
|
|
150
|
+
def check_pattern(pattern, prefix)
|
|
151
|
+
return ["#{prefix}: pattern missing or empty"] if pattern.nil? || blank?(pattern)
|
|
152
|
+
|
|
153
|
+
Regexp.new(pattern)
|
|
154
|
+
return ["#{prefix}: pattern '#{pattern}' is degenerate (matches everything)"] if degenerate?(pattern)
|
|
155
|
+
|
|
156
|
+
[]
|
|
157
|
+
rescue RegexpError => e
|
|
158
|
+
["#{prefix}: pattern does not compile: #{e.message}"]
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
# Validates the applies_to field.
|
|
162
|
+
#
|
|
163
|
+
# @param applies_to [Array, nil]
|
|
164
|
+
# @param prefix [String] error message prefix
|
|
165
|
+
# @return [Array<String>]
|
|
166
|
+
def check_applies_to(applies_to, prefix)
|
|
167
|
+
return ["#{prefix}: applies_to missing or empty"] unless applies_to.is_a?(Array) && !applies_to.empty?
|
|
168
|
+
|
|
169
|
+
[]
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
# Validates the action field.
|
|
173
|
+
#
|
|
174
|
+
# @param action [String, nil]
|
|
175
|
+
# @param prefix [String] error message prefix
|
|
176
|
+
# @return [Array<String>]
|
|
177
|
+
def check_action(action, prefix)
|
|
178
|
+
return [] if VALID_ACTIONS.include?(action)
|
|
179
|
+
|
|
180
|
+
allowed = VALID_ACTIONS.join(', ')
|
|
181
|
+
["#{prefix}: action '#{action}' invalid (must be one of: #{allowed})"]
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
# Tests whether a string is blank (nil, empty, or whitespace-only).
|
|
185
|
+
#
|
|
186
|
+
# @param value [String]
|
|
187
|
+
# @return [Boolean]
|
|
188
|
+
def blank?(value)
|
|
189
|
+
value.is_a?(String) && value.strip.empty?
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
# Tests whether a pattern is degenerate (matches everything).
|
|
193
|
+
#
|
|
194
|
+
# @param pattern [String] regex pattern string
|
|
195
|
+
# @return [Boolean]
|
|
196
|
+
def degenerate?(pattern)
|
|
197
|
+
DEGENERATE_PATTERNS.any? { |dp| dp.match?(pattern.strip) }
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
# Builds a valid entry hash for an enforceable rule.
|
|
201
|
+
#
|
|
202
|
+
# @param rule [Hash]
|
|
203
|
+
# @param enforcement [Hash]
|
|
204
|
+
# @param ctx [RuleContext]
|
|
205
|
+
# @return [Hash]
|
|
206
|
+
def build_valid_entry(rule, enforcement, ctx)
|
|
207
|
+
{ rule_id: ctx.rule_id, behaviour: ctx.behaviour, type: 'enforceable',
|
|
208
|
+
pattern: enforcement['pattern'], applies_to: enforcement['applies_to'],
|
|
209
|
+
action: enforcement['action'], message: enforcement['message'],
|
|
210
|
+
priority: rule['priority'] || 0, description: rule['description'].to_s.strip }
|
|
211
|
+
end
|
|
212
|
+
end
|
|
213
|
+
end
|
|
214
|
+
end
|
|
215
|
+
end
|