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