woods 1.6.1 → 2.0.0.beta2
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 +4 -4
- data/CHANGELOG.md +2035 -0
- data/CONTRIBUTING.md +253 -87
- data/README.md +161 -513
- data/SECURITY.md +92 -0
- data/assets/woods-wordmark-white-with-bg.png +0 -0
- data/docs/AGENT_GUIDE.md +204 -0
- data/docs/AGENT_SETUP.md +205 -0
- data/docs/BACKEND_MATRIX.md +470 -0
- data/docs/CONFIGURATION_REFERENCE.md +655 -0
- data/docs/CONSOLE_MCP_SETUP.md +829 -0
- data/docs/DOCKER_SETUP.md +454 -0
- data/docs/EMBEDDING_MODELS.md +136 -0
- data/docs/EVALUATION.md +91 -0
- data/docs/EXTRACTOR_REFERENCE.md +765 -0
- data/docs/FAQ.md +544 -0
- data/docs/GETTING_STARTED.md +183 -0
- data/docs/INCREMENTAL_EXTRACTION.md +455 -0
- data/docs/INTERNALS.md +418 -0
- data/docs/MCP_HTTP_TRANSPORT.md +144 -0
- data/docs/MCP_SERVERS.md +231 -0
- data/docs/MCP_TOOL_COOKBOOK.md +987 -0
- data/docs/MCP_WORKTREE_SETUP.md +127 -0
- data/docs/NOTION_INTEGRATION.md +283 -0
- data/docs/OBSIDIAN_INTEGRATION.md +170 -0
- data/docs/PUBLISHED_INDEX.md +213 -0
- data/docs/README.md +94 -0
- data/docs/RETRIEVAL_GUIDE.md +267 -0
- data/docs/TOKEN_BENCHMARK.md +68 -0
- data/docs/TROUBLESHOOTING.md +841 -0
- data/docs/UNBLOCKED_INTEGRATION.md +279 -0
- data/docs/UPGRADING_TO_2.md +321 -0
- data/docs/WATCH_DAEMON.md +667 -0
- data/docs/WHY_WOODS.md +219 -0
- data/exe/woods-console +40 -4
- data/exe/woods-console-mcp +21 -35
- data/exe/woods-mcp +20 -7
- data/exe/woods-mcp-http +80 -11
- data/exe/woods-mcp-start +57 -52
- data/lib/generators/woods/install_generator.rb +6 -5
- data/lib/generators/woods/pgvector_generator.rb +6 -3
- data/lib/generators/woods/templates/add_pgvector_to_woods.rb.erb +29 -9
- data/lib/generators/woods/templates/create_woods_tables.rb.erb +5 -1
- data/lib/generators/woods/templates/woods.rb.tt +49 -28
- data/lib/tasks/woods.rake +622 -168
- data/lib/tasks/woods_checks.rake +107 -0
- data/lib/tasks/woods_evaluation.rake +164 -80
- data/lib/woods/ast/call_site_extractor.rb +6 -15
- data/lib/woods/ast/method_extractor.rb +19 -9
- data/lib/woods/ast/parser.rb +54 -8
- data/lib/woods/atomic_file.rb +171 -2
- data/lib/woods/builder.rb +310 -22
- data/lib/woods/cache/cache_middleware.rb +7 -2
- data/lib/woods/cache/cache_store.rb +9 -1
- data/lib/woods/cache/solid_cache_store.rb +6 -4
- data/lib/woods/change_set.rb +88 -0
- data/lib/woods/checks/generation_resolution.rb +34 -0
- data/lib/woods/checks/moved_messages.rb +186 -0
- data/lib/woods/chunking/semantic_chunker.rb +160 -18
- data/lib/woods/console/audit_logger.rb +12 -3
- data/lib/woods/console/bridge_protocol.rb +3 -16
- data/lib/woods/console/connection_manager.rb +51 -136
- data/lib/woods/console/dispatch_pipeline.rb +42 -12
- data/lib/woods/console/embedded_executor.rb +806 -149
- data/lib/woods/console/eval_guard.rb +27 -20
- data/lib/woods/console/input_contract.rb +78 -0
- data/lib/woods/console/model_validator.rb +29 -1
- data/lib/woods/console/rack_middleware.rb +65 -42
- data/lib/woods/console/redactor.rb +26 -8
- data/lib/woods/console/safe_context.rb +58 -10
- data/lib/woods/console/scope_predicate_parser.rb +41 -0
- data/lib/woods/console/server.rb +119 -247
- data/lib/woods/console/sql_noise_stripper.rb +125 -16
- data/lib/woods/console/sql_table_scanner.rb +82 -22
- data/lib/woods/console/sql_validator.rb +459 -29
- data/lib/woods/console/table_gate.rb +2 -2
- data/lib/woods/console/tool_specs.rb +463 -90
- data/lib/woods/console/tools/tier1.rb +1 -5
- data/lib/woods/console/tools/tier4.rb +18 -9
- data/lib/woods/coordination/lock_heartbeat.rb +103 -0
- data/lib/woods/coordination/pipeline_lock.rb +263 -53
- data/lib/woods/db/migrations/007_typed_snapshot_units.rb +45 -0
- data/lib/woods/db/migrator.rb +3 -9
- data/lib/woods/db/schema_version.rb +47 -2
- data/lib/woods/dependency_graph.rb +898 -64
- data/lib/woods/embedding/fake.rb +138 -0
- data/lib/woods/embedding/indexer.rb +832 -40
- data/lib/woods/embedding/openai.rb +77 -19
- data/lib/woods/embedding/provider.rb +189 -11
- data/lib/woods/embedding/text_preparer.rb +1 -1
- data/lib/woods/embedding/token_counter.rb +0 -7
- data/lib/woods/evaluation/ablation_agent_payload.rb +38 -0
- data/lib/woods/evaluation/ablation_executor.rb +67 -0
- data/lib/woods/evaluation/ablation_provenance.rb +38 -0
- data/lib/woods/evaluation/ablation_report_writer.rb +43 -0
- data/lib/woods/evaluation/ablation_runner.rb +173 -0
- data/lib/woods/evaluation/ablation_summary.rb +65 -0
- data/lib/woods/evaluation/ablation_task.rb +66 -0
- data/lib/woods/evaluation/ablation_task_set.rb +77 -0
- data/lib/woods/evaluation/ablation_timed_executor.rb +91 -0
- data/lib/woods/evaluation/ablation_worktree.rb +71 -0
- data/lib/woods/evaluation/baseline.rb +60 -0
- data/lib/woods/evaluation/baseline_runner.rb +11 -3
- data/lib/woods/evaluation/evaluator.rb +41 -8
- data/lib/woods/evaluation/query_set.rb +79 -13
- data/lib/woods/evaluation/report_generator.rb +20 -1
- data/lib/woods/export/unit_facts.rb +0 -11
- data/lib/woods/extracted_unit.rb +22 -63
- data/lib/woods/extractor.rb +2783 -238
- data/lib/woods/extractors/action_cable_extractor.rb +9 -4
- data/lib/woods/extractors/ast_source_extraction.rb +20 -2
- data/lib/woods/extractors/caching_extractor.rb +46 -12
- data/lib/woods/extractors/callback_analyzer.rb +39 -9
- data/lib/woods/extractors/component_discovery.rb +123 -0
- data/lib/woods/extractors/concern_extractor.rb +17 -3
- data/lib/woods/extractors/controller_extractor.rb +389 -29
- data/lib/woods/extractors/decorator_extractor.rb +7 -14
- data/lib/woods/extractors/engine_extractor.rb +53 -8
- data/lib/woods/extractors/event_extractor.rb +55 -4
- data/lib/woods/extractors/factory_extractor.rb +49 -11
- data/lib/woods/extractors/graphql_extractor.rb +162 -66
- data/lib/woods/extractors/i18n_extractor.rb +6 -1
- data/lib/woods/extractors/job_extractor.rb +51 -21
- data/lib/woods/extractors/lib_extractor.rb +23 -17
- data/lib/woods/extractors/line_neutralizer.rb +171 -0
- data/lib/woods/extractors/mailer_extractor.rb +9 -1
- data/lib/woods/extractors/manager_extractor.rb +19 -2
- data/lib/woods/extractors/migration_extractor.rb +22 -11
- data/lib/woods/extractors/model_extractor.rb +292 -57
- data/lib/woods/extractors/package_extractor.rb +154 -0
- data/lib/woods/extractors/phlex_extractor.rb +18 -3
- data/lib/woods/extractors/policy_extractor.rb +6 -5
- data/lib/woods/extractors/poro_extractor.rb +13 -14
- data/lib/woods/extractors/pundit_extractor.rb +3 -3
- data/lib/woods/extractors/rails_source_extractor.rb +24 -7
- data/lib/woods/extractors/rake_task_extractor.rb +158 -30
- data/lib/woods/extractors/reference_patterns.rb +38 -0
- data/lib/woods/extractors/route_extractor.rb +58 -2
- data/lib/woods/extractors/scheduled_job_extractor.rb +51 -35
- data/lib/woods/extractors/serializer_extractor.rb +3 -4
- data/lib/woods/extractors/service_extractor.rb +11 -1
- data/lib/woods/extractors/shared_dependency_scanner.rb +24 -34
- data/lib/woods/extractors/shared_utility_methods.rb +36 -6
- data/lib/woods/extractors/source_nesting.rb +560 -0
- data/lib/woods/extractors/state_machine_extractor.rb +30 -18
- data/lib/woods/extractors/test_mapping_extractor.rb +26 -9
- data/lib/woods/extractors/view_component_extractor.rb +28 -3
- data/lib/woods/extractors/view_engines/erb.rb +17 -3
- data/lib/woods/feedback/gap_detector.rb +9 -3
- data/lib/woods/feedback/store.rb +7 -1
- data/lib/woods/filename_utils.rb +29 -1
- data/lib/woods/flow_analysis/operation_extractor.rb +22 -10
- data/lib/woods/flow_assembler.rb +147 -26
- data/lib/woods/flow_document.rb +1 -0
- data/lib/woods/flow_precomputer.rb +175 -22
- data/lib/woods/gem_mapper.rb +285 -0
- data/lib/woods/generation.rb +185 -0
- data/lib/woods/git_command.rb +38 -0
- data/lib/woods/git_provenance.rb +16 -2
- data/lib/woods/graph_analyzer.rb +564 -87
- data/lib/woods/index_artifact.rb +93 -23
- data/lib/woods/mcp/bearer_auth.rb +102 -13
- data/lib/woods/mcp/bootstrap_state.rb +77 -0
- data/lib/woods/mcp/bootstrapper.rb +582 -77
- data/lib/woods/mcp/config_resolver.rb +66 -6
- data/lib/woods/mcp/errors.rb +60 -0
- data/lib/woods/mcp/index_reader.rb +836 -117
- data/lib/woods/mcp/index_reader_pinning.rb +78 -0
- data/lib/woods/mcp/origin_guard.rb +66 -7
- data/lib/woods/mcp/protocol_policy.rb +98 -0
- data/lib/woods/mcp/provider_probe.rb +45 -6
- data/lib/woods/mcp/renderers/markdown_renderer.rb +72 -4
- data/lib/woods/mcp/renderers/plain_renderer.rb +54 -6
- data/lib/woods/mcp/server.rb +898 -152
- data/lib/woods/mcp/tasks/extension.rb +196 -0
- data/lib/woods/mcp/tasks/request_capture.rb +45 -0
- data/lib/woods/mcp/tasks/store.rb +518 -0
- data/lib/woods/mcp/tool_contract.rb +171 -0
- data/lib/woods/mcp/tool_response_renderer.rb +7 -0
- data/lib/woods/model_name_cache.rb +19 -1
- data/lib/woods/notion/client.rb +132 -36
- data/lib/woods/notion/exporter.rb +456 -61
- data/lib/woods/notion/mappers/column_mapper.rb +34 -5
- data/lib/woods/notion/mappers/migration_mapper.rb +32 -8
- data/lib/woods/notion/mappers/model_mapper.rb +21 -6
- data/lib/woods/notion/mappers/shared.rb +45 -3
- data/lib/woods/notion/sync_manifest.rb +258 -0
- data/lib/woods/obsidian/errors.rb +6 -0
- data/lib/woods/obsidian/name_mapper.rb +40 -24
- data/lib/woods/obsidian/vault_exporter.rb +103 -36
- data/lib/woods/operator/pipeline_guard.rb +118 -21
- data/lib/woods/operator/status_reporter.rb +20 -3
- data/lib/woods/path_dispatcher.rb +276 -0
- data/lib/woods/payload_store.rb +236 -0
- data/lib/woods/published_index/edge_shaper.rb +61 -0
- data/lib/woods/published_index/generation_catalog.rb +72 -0
- data/lib/woods/published_index/typed_unit_reader.rb +48 -0
- data/lib/woods/published_index.rb +287 -0
- data/lib/woods/railtie.rb +69 -30
- data/lib/woods/railtie_support.rb +167 -0
- data/lib/woods/release.rb +12 -0
- data/lib/woods/reload_policy.rb +206 -0
- data/lib/woods/resilience/circuit_breaker.rb +47 -8
- data/lib/woods/resilience/index_validator.rb +296 -10
- data/lib/woods/resilience/retryable_provider.rb +71 -6
- data/lib/woods/resolved_config.rb +55 -11
- data/lib/woods/retrieval/context_assembler.rb +132 -40
- data/lib/woods/retrieval/query_classifier.rb +26 -8
- data/lib/woods/retrieval/ranker.rb +193 -28
- data/lib/woods/retrieval/search_executor.rb +206 -39
- data/lib/woods/retriever.rb +317 -71
- data/lib/woods/retry_after.rb +22 -2
- data/lib/woods/ruby_analyzer/class_analyzer.rb +10 -14
- data/lib/woods/ruby_analyzer/fqn_builder.rb +2 -0
- data/lib/woods/ruby_analyzer/mermaid_renderer.rb +14 -4
- data/lib/woods/ruby_analyzer/method_analyzer.rb +1 -1
- data/lib/woods/ruby_analyzer/trace_enricher.rb +3 -0
- data/lib/woods/ruby_analyzer.rb +21 -5
- data/lib/woods/session_tracer/file_store.rb +138 -19
- data/lib/woods/session_tracer/middleware.rb +1 -2
- data/lib/woods/session_tracer/redis_store.rb +122 -12
- data/lib/woods/session_tracer/session_flow_assembler.rb +57 -17
- data/lib/woods/session_tracer/session_flow_document.rb +56 -14
- data/lib/woods/session_tracer/solid_cache_coordination.rb +192 -0
- data/lib/woods/session_tracer/solid_cache_store.rb +560 -91
- data/lib/woods/session_tracer/store.rb +14 -1
- data/lib/woods/storage/metadata_store.rb +230 -26
- data/lib/woods/storage/pgvector.rb +180 -22
- data/lib/woods/storage/qdrant.rb +367 -41
- data/lib/woods/storage/snapshotter/metadata.rb +79 -16
- data/lib/woods/storage/snapshotter/vector.rb +128 -17
- data/lib/woods/storage/snapshotter.rb +23 -5
- data/lib/woods/storage/vector_store.rb +49 -8
- data/lib/woods/storage_identity.rb +28 -0
- data/lib/woods/tasks.rb +53 -2
- data/lib/woods/temporal/json_snapshot_store.rb +112 -42
- data/lib/woods/temporal/snapshot_store.rb +139 -42
- data/lib/woods/unblocked/client.rb +119 -17
- data/lib/woods/unblocked/document_builder.rb +34 -2
- data/lib/woods/unblocked/exporter.rb +63 -27
- data/lib/woods/unblocked/rate_limiter.rb +23 -9
- data/lib/woods/unblocked/sync_manifest.rb +16 -8
- data/lib/woods/update_check.rb +24 -1
- data/lib/woods/util/uuid5.rb +124 -0
- data/lib/woods/version.rb +1 -1
- data/lib/woods/watch/daemon.rb +1345 -0
- data/lib/woods/watch/listen_watcher.rb +81 -0
- data/lib/woods/watch/polling_watcher.rb +137 -0
- data/lib/woods/watch/status.rb +169 -0
- data/lib/woods/watch/tree_scan.rb +163 -0
- data/lib/woods/watch/watcher.rb +100 -0
- data/lib/woods.rb +138 -9
- data/plugin/.claude-plugin/plugin.json +18 -0
- data/plugin/hooks/hooks.json +29 -0
- data/plugin/hooks/woods-post-edit.sh +226 -0
- data/plugin/hooks/woods-session-start.sh +77 -0
- data/plugin/skills/woods-agent-enable/SKILL.md +51 -0
- data/plugin/skills/woods-diagnose/SKILL.md +75 -0
- data/plugin/skills/woods-investigate/SKILL.md +39 -0
- data/plugin/skills/woods-mcp-config/SKILL.md +101 -0
- data/plugin/skills/woods-setup/SKILL.md +99 -0
- metadata +134 -23
- data/lib/woods/console/adapters/cache_adapter.rb +0 -58
- data/lib/woods/console/adapters/good_job_adapter.rb +0 -33
- data/lib/woods/console/adapters/job_adapter.rb +0 -74
- data/lib/woods/console/adapters/sidekiq_adapter.rb +0 -33
- data/lib/woods/console/adapters/solid_queue_adapter.rb +0 -33
- data/lib/woods/console/bridge.rb +0 -210
- data/lib/woods/formatting/claude_adapter.rb +0 -98
- data/lib/woods/formatting/generic_adapter.rb +0 -56
- data/lib/woods/formatting/gpt_adapter.rb +0 -64
- data/lib/woods/notion/mapper.rb +0 -40
- data/lib/woods/observability/health_check.rb +0 -79
- data/lib/woods/observability/instrumentation.rb +0 -34
|
@@ -1,11 +1,16 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require 'mcp'
|
|
4
|
+
|
|
5
|
+
require_relative 'input_contract'
|
|
6
|
+
require_relative 'scope_predicate_parser'
|
|
7
|
+
|
|
3
8
|
# This file is a pure data table — 31 ToolSpec entries across 4 tiers
|
|
4
9
|
# (9 read-only / 9 domain-aware / 10 analytics / 3 guarded).
|
|
5
10
|
# Metrics/ModuleLength is disabled here because the module body is almost
|
|
6
11
|
# entirely declarative data, not imperative logic. Decomposition would just
|
|
7
12
|
# scatter the tool catalogue across many files with no readability gain.
|
|
8
|
-
# rubocop:disable Metrics/ModuleLength
|
|
13
|
+
# rubocop:disable-next Metrics/ModuleLength
|
|
9
14
|
module Woods
|
|
10
15
|
module Console
|
|
11
16
|
module Server
|
|
@@ -16,6 +21,67 @@ module Woods
|
|
|
16
21
|
job_schedule redis_info cache_stats channel_status].freeze
|
|
17
22
|
TIER4_TOOLS = %w[eval sql query].freeze
|
|
18
23
|
|
|
24
|
+
MIN_INTEGER_INPUT = 1
|
|
25
|
+
MAX_RECORD_ID = 9_223_372_036_854_775_807
|
|
26
|
+
AGGREGATE_FUNCTIONS = %w[sum average minimum maximum count].freeze
|
|
27
|
+
|
|
28
|
+
SAFE_IDENTIFIER_GRAMMAR = '[A-Za-z_][A-Za-z0-9_]*'
|
|
29
|
+
NON_WHITESPACE_SCHEMA_PATTERN = '\\S'
|
|
30
|
+
COLUMN_REFERENCE_GRAMMAR =
|
|
31
|
+
"#{SAFE_IDENTIFIER_GRAMMAR}(?:\\.#{SAFE_IDENTIFIER_GRAMMAR})?".freeze
|
|
32
|
+
SAFE_IDENTIFIER_SCHEMA_PATTERN = "^(?:#{SAFE_IDENTIFIER_GRAMMAR})$(?![\\s\\S])".freeze
|
|
33
|
+
COLUMN_REFERENCE_SCHEMA_PATTERN = "^(?:#{COLUMN_REFERENCE_GRAMMAR})$(?![\\s\\S])".freeze
|
|
34
|
+
SAFE_IDENTIFIER_REGEXP = Regexp.new("\\A(?:#{SAFE_IDENTIFIER_GRAMMAR})\\z").freeze
|
|
35
|
+
SCOPE_PREDICATE_SUFFIX_GRAMMAR =
|
|
36
|
+
"(?:#{ScopePredicateParser::SUPPORTED_SUFFIXES.map { |suffix| Regexp.escape(suffix) }.join('|')})".freeze
|
|
37
|
+
SCOPE_KEY_GRAMMAR = "#{SAFE_IDENTIFIER_GRAMMAR}(?:#{SCOPE_PREDICATE_SUFFIX_GRAMMAR})?".freeze
|
|
38
|
+
SCOPE_KEY_SCHEMA_PATTERN = "^(?:#{SCOPE_KEY_GRAMMAR})$(?![\\s\\S])".freeze
|
|
39
|
+
|
|
40
|
+
# Suffix-dependent value types for scope predicates, shared by every
|
|
41
|
+
# `scope:` property below. Without this, a JSON boolean-looking string
|
|
42
|
+
# (e.g. `"false"`) reaches the executor for an existence suffix
|
|
43
|
+
# (`_null`/`_present`/`_blank`) and inverts on Ruby truthiness — any
|
|
44
|
+
# non-empty String is truthy, so `"false"` behaves as `true`.
|
|
45
|
+
SCOPE_VALUE_PATTERN_PROPERTIES = {
|
|
46
|
+
"(#{Regexp.union(ScopePredicateParser::EXISTENCE_SUFFIXES).source})$" => { type: 'boolean' },
|
|
47
|
+
"(#{Regexp.union(ScopePredicateParser::COMPARISON_SUFFIXES).source})$" => { type: %w[string number] },
|
|
48
|
+
"(#{Regexp.union(ScopePredicateParser::SET_SUFFIXES).source})$" => { type: 'array' }
|
|
49
|
+
}.freeze
|
|
50
|
+
|
|
51
|
+
CASE_INSENSITIVE_AGGREGATE_GRAMMAR = [
|
|
52
|
+
'[Ss][Uu][Mm]', '[Aa][Vv][Gg]', '[Mm][Ii][Nn]', '[Mm][Aa][Xx]', '[Cc][Oo][Uu][Nn][Tt]'
|
|
53
|
+
].join('|').freeze
|
|
54
|
+
SELECT_EXPRESSION_GRAMMAR = [
|
|
55
|
+
"\\s*(?:((?:#{CASE_INSENSITIVE_AGGREGATE_GRAMMAR}))\\s*\\(\\s*",
|
|
56
|
+
"(\\*|#{COLUMN_REFERENCE_GRAMMAR})\\s*\\)|",
|
|
57
|
+
"(#{COLUMN_REFERENCE_GRAMMAR}))(?:\\s+[Aa][Ss]\\s+(#{SAFE_IDENTIFIER_GRAMMAR}))?\\s*"
|
|
58
|
+
].join.freeze
|
|
59
|
+
SELECT_EXPRESSION_SCHEMA_PATTERN = "^(?:#{SELECT_EXPRESSION_GRAMMAR})$(?![\\s\\S])".freeze
|
|
60
|
+
SELECT_EXPRESSION_REGEXP = Regexp.new("\\A(?:#{SELECT_EXPRESSION_GRAMMAR})\\z").freeze
|
|
61
|
+
|
|
62
|
+
ORDER_DIRECTION_GRAMMAR = '(?:[Aa][Ss][Cc]|[Dd][Ee][Ss][Cc])'
|
|
63
|
+
ORDER_DIRECTION_SCHEMA_PATTERN = "^(?:#{ORDER_DIRECTION_GRAMMAR})$(?![\\s\\S])".freeze
|
|
64
|
+
ORDER_DIRECTION_REGEXP = Regexp.new("\\A(?:#{ORDER_DIRECTION_GRAMMAR})\\z").freeze
|
|
65
|
+
|
|
66
|
+
# Authoritative grammar shared by the public JSON Schema and executor.
|
|
67
|
+
# Schema and Ruby use different end-anchor syntax, but the executable
|
|
68
|
+
# template body is defined once so accepted SQL cannot drift.
|
|
69
|
+
HAVING_TEMPLATE_GRAMMAR = [
|
|
70
|
+
"\\s*(?:(#{COLUMN_REFERENCE_GRAMMAR})|",
|
|
71
|
+
'(SUM|AVG|MIN|MAX|COUNT)\\s*\\(\\s*',
|
|
72
|
+
"(\\*|#{COLUMN_REFERENCE_GRAMMAR})\\s*\\))",
|
|
73
|
+
'\\s*(=|!=|<>|<=|>=|<|>)\\s*\\?\\s*'
|
|
74
|
+
].join.freeze
|
|
75
|
+
HAVING_TEMPLATE_SCHEMA_PATTERN = "^(?:#{HAVING_TEMPLATE_GRAMMAR})$(?![\\s\\S])".freeze
|
|
76
|
+
HAVING_TEMPLATE_REGEXP = Regexp.new("\\A(?:#{HAVING_TEMPLATE_GRAMMAR})\\z").freeze
|
|
77
|
+
|
|
78
|
+
QUERY_SCOPE_TEMPLATE_GRAMMAR = [
|
|
79
|
+
"\\s*(#{COLUMN_REFERENCE_GRAMMAR})\\s*",
|
|
80
|
+
'(?:=|!=|<>|<=|>=|<|>)\\s*\\?\\s*'
|
|
81
|
+
].join.freeze
|
|
82
|
+
QUERY_SCOPE_TEMPLATE_SCHEMA_PATTERN = "^(?:#{QUERY_SCOPE_TEMPLATE_GRAMMAR})$(?![\\s\\S])".freeze
|
|
83
|
+
QUERY_SCOPE_TEMPLATE_REGEXP = Regexp.new("\\A(?:#{QUERY_SCOPE_TEMPLATE_GRAMMAR})\\z").freeze
|
|
84
|
+
|
|
19
85
|
# Value object that holds a single MCP tool's declarative specification.
|
|
20
86
|
#
|
|
21
87
|
# @!attribute [r] name
|
|
@@ -32,34 +98,67 @@ module Woods
|
|
|
32
98
|
# @return [Proc] Lambda called with symbolised args hash; returns the
|
|
33
99
|
# request Hash forwarded to the bridge/executor. Any tier-specific
|
|
34
100
|
# objects (validators, guards) are captured in the lambda's closure.
|
|
35
|
-
ToolSpec = Struct.new(
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
101
|
+
ToolSpec = Struct.new(
|
|
102
|
+
:name, :description, :properties, :required, :tier, :handler, :schema_constraints,
|
|
103
|
+
keyword_init: true
|
|
104
|
+
) do
|
|
105
|
+
def input_schema
|
|
106
|
+
schema = { properties: properties }
|
|
107
|
+
schema[:required] = required if required&.any?
|
|
108
|
+
schema.merge!(schema_constraints) if schema_constraints
|
|
109
|
+
schema
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def validate_arguments!(arguments)
|
|
113
|
+
schema = input_schema_value
|
|
114
|
+
if arguments.is_a?(Hash)
|
|
115
|
+
missing = schema.missing_required_arguments(arguments)
|
|
116
|
+
unless missing.empty?
|
|
117
|
+
raise InputContract::ValidationError, "Missing required arguments: #{missing.join(', ')}"
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
schema.validate_arguments(arguments)
|
|
122
|
+
arguments
|
|
123
|
+
rescue ::MCP::Tool::InputSchema::ValidationError => e
|
|
124
|
+
raise InputContract::ValidationError, e.message
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def wrap_handler!
|
|
128
|
+
raw_handler = handler
|
|
129
|
+
self.handler = lambda do |arguments|
|
|
130
|
+
validate_arguments!(arguments)
|
|
131
|
+
raw_handler.call(arguments)
|
|
132
|
+
end
|
|
133
|
+
self
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
private
|
|
137
|
+
|
|
138
|
+
def input_schema_value
|
|
139
|
+
@input_schema_value ||= ::MCP::Tool::InputSchema.new(input_schema)
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
# All 31 possible Console tool specifications, grouped by tier. Runtime
|
|
144
|
+
# registration is derived separately in EXECUTABLE_MODES: Tier 1 is
|
|
145
|
+
# executable in both embedded modes, and sql/query are executable only
|
|
146
|
+
# in explicit embedded-read mode. Tier 2, Tier 3, and eval remain an
|
|
147
|
+
# unregistered inventory until real executors and enforceable controls
|
|
148
|
+
# exist for them.
|
|
52
149
|
TOOL_SPECS = [
|
|
53
150
|
# ── Tier 1: read-only ─────────────────────────────────────────────────
|
|
54
151
|
ToolSpec.new(
|
|
55
152
|
name: 'console_count',
|
|
56
153
|
description: 'Count records matching scope conditions.',
|
|
57
154
|
properties: {
|
|
58
|
-
model: { type: 'string', description: 'Model name' },
|
|
59
|
-
scope: { type: 'object',
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
155
|
+
model: { type: 'string', pattern: NON_WHITESPACE_SCHEMA_PATTERN, description: 'Model name' },
|
|
156
|
+
scope: { type: 'object', propertyNames: { pattern: SCOPE_KEY_SCHEMA_PATTERN },
|
|
157
|
+
patternProperties: SCOPE_VALUE_PATTERN_PROPERTIES,
|
|
158
|
+
description: 'Filter: {status: "paid", total_refund_gt: 0, ' \
|
|
159
|
+
'transaction_id_not_null: true}. ' \
|
|
160
|
+
'Suffixes: _eq _gt _lt _in _null _present. ' \
|
|
161
|
+
'Complex queries: use console_query.' }
|
|
63
162
|
},
|
|
64
163
|
required: ['model'],
|
|
65
164
|
tier: 1,
|
|
@@ -69,12 +168,16 @@ module Woods
|
|
|
69
168
|
name: 'console_sample',
|
|
70
169
|
description: 'Random sample of records.',
|
|
71
170
|
properties: {
|
|
72
|
-
model: { type: 'string', description: 'Model name' },
|
|
73
|
-
limit: { type: 'integer',
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
171
|
+
model: { type: 'string', pattern: NON_WHITESPACE_SCHEMA_PATTERN, description: 'Model name' },
|
|
172
|
+
limit: { type: 'integer', minimum: MIN_INTEGER_INPUT, maximum: 25,
|
|
173
|
+
description: 'Max records (default 5, max 25)' },
|
|
174
|
+
columns: { type: 'array', items: { type: 'string', pattern: SAFE_IDENTIFIER_SCHEMA_PATTERN },
|
|
175
|
+
description: 'Columns to include' },
|
|
176
|
+
scope: { type: 'object', propertyNames: { pattern: SCOPE_KEY_SCHEMA_PATTERN },
|
|
177
|
+
patternProperties: SCOPE_VALUE_PATTERN_PROPERTIES,
|
|
178
|
+
description: 'Filter: {status: "paid", amount_gt: 100}. ' \
|
|
179
|
+
'Suffixes: _eq _gt _lt _in _null _present. ' \
|
|
180
|
+
'Complex queries: use console_query.' }
|
|
78
181
|
},
|
|
79
182
|
required: ['model'],
|
|
80
183
|
tier: 1,
|
|
@@ -88,13 +191,27 @@ module Woods
|
|
|
88
191
|
name: 'console_find',
|
|
89
192
|
description: 'Find a single record by primary key or unique column',
|
|
90
193
|
properties: {
|
|
91
|
-
model: { type: 'string', description: 'Model name' },
|
|
92
|
-
id: { type: 'integer',
|
|
93
|
-
|
|
94
|
-
|
|
194
|
+
model: { type: 'string', pattern: NON_WHITESPACE_SCHEMA_PATTERN, description: 'Model name' },
|
|
195
|
+
id: { type: 'integer', minimum: MIN_INTEGER_INPUT, maximum: MAX_RECORD_ID,
|
|
196
|
+
description: 'Primary key value' },
|
|
197
|
+
by: { type: 'object', minProperties: 1, propertyNames: { pattern: SAFE_IDENTIFIER_SCHEMA_PATTERN },
|
|
198
|
+
description: 'Unique column lookup (must be non-empty)' },
|
|
199
|
+
columns: { type: 'array', items: { type: 'string', pattern: SAFE_IDENTIFIER_SCHEMA_PATTERN },
|
|
200
|
+
description: 'Columns to include' }
|
|
95
201
|
},
|
|
96
202
|
required: ['model'],
|
|
97
203
|
tier: 1,
|
|
204
|
+
# Exactly one non-empty locator form: `id` XOR a non-empty `by`.
|
|
205
|
+
# `find_by({})` (no locator at all) returns an arbitrary row:
|
|
206
|
+
# this constraint is the schema half of closing that gap; the
|
|
207
|
+
# executor enforces it again as defense-in-depth (see
|
|
208
|
+
# EmbeddedExecutor#validate_find_locator!).
|
|
209
|
+
schema_constraints: {
|
|
210
|
+
oneOf: [
|
|
211
|
+
{ required: ['id'] },
|
|
212
|
+
{ required: ['by'] }
|
|
213
|
+
]
|
|
214
|
+
},
|
|
98
215
|
handler: lambda { |args|
|
|
99
216
|
Tools::Tier1.console_find(
|
|
100
217
|
model: args[:model], id: args[:id], by: args[:by], columns: args[:columns]
|
|
@@ -105,12 +222,17 @@ module Woods
|
|
|
105
222
|
name: 'console_pluck',
|
|
106
223
|
description: 'Extract column values from records.',
|
|
107
224
|
properties: {
|
|
108
|
-
model: { type: 'string', description: 'Model name' },
|
|
109
|
-
columns: { type: 'array',
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
225
|
+
model: { type: 'string', pattern: NON_WHITESPACE_SCHEMA_PATTERN, description: 'Model name' },
|
|
226
|
+
columns: { type: 'array', minItems: 1,
|
|
227
|
+
items: { type: 'string', pattern: SAFE_IDENTIFIER_SCHEMA_PATTERN },
|
|
228
|
+
description: 'Column names to pluck' },
|
|
229
|
+
scope: { type: 'object', propertyNames: { pattern: SCOPE_KEY_SCHEMA_PATTERN },
|
|
230
|
+
patternProperties: SCOPE_VALUE_PATTERN_PROPERTIES,
|
|
231
|
+
description: 'Filter: {status_in: ["paid","refunded"], amount_gt: 0}. ' \
|
|
232
|
+
'Suffixes: _eq _gt _lt _in _null _present. ' \
|
|
233
|
+
'Complex queries: use console_query.' },
|
|
234
|
+
limit: { type: 'integer', minimum: MIN_INTEGER_INPUT, maximum: 1000,
|
|
235
|
+
description: 'Max records (default 100, max 1000)' },
|
|
114
236
|
distinct: { type: 'boolean', description: 'Return unique values only' }
|
|
115
237
|
},
|
|
116
238
|
required: %w[model columns],
|
|
@@ -129,14 +251,29 @@ module Woods
|
|
|
129
251
|
'Supports scope predicates: {status: "paid", total_gt: 0}. ' \
|
|
130
252
|
'For complex queries use console_query.',
|
|
131
253
|
properties: {
|
|
132
|
-
model: { type: 'string', description: 'Model name' },
|
|
133
|
-
function: { type: 'string',
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
254
|
+
model: { type: 'string', pattern: NON_WHITESPACE_SCHEMA_PATTERN, description: 'Model name' },
|
|
255
|
+
function: { type: 'string', enum: AGGREGATE_FUNCTIONS,
|
|
256
|
+
description: 'Aggregate function: sum, average, minimum, maximum, count' },
|
|
257
|
+
column: { type: 'string', pattern: SAFE_IDENTIFIER_SCHEMA_PATTERN,
|
|
258
|
+
description: 'Column to aggregate (optional for count)' },
|
|
259
|
+
scope: { type: 'object', propertyNames: { pattern: SCOPE_KEY_SCHEMA_PATTERN },
|
|
260
|
+
patternProperties: SCOPE_VALUE_PATTERN_PROPERTIES,
|
|
261
|
+
description: 'Filter conditions: {col: val} or predicate suffixes ' \
|
|
262
|
+
'(_gt, _lt, _in, _null, etc.)' }
|
|
137
263
|
},
|
|
138
264
|
required: %w[model function],
|
|
139
265
|
tier: 1,
|
|
266
|
+
schema_constraints: {
|
|
267
|
+
allOf: [
|
|
268
|
+
{
|
|
269
|
+
if: {
|
|
270
|
+
properties: { function: { enum: AGGREGATE_FUNCTIONS.reject { |name| name == 'count' } } },
|
|
271
|
+
required: ['function']
|
|
272
|
+
},
|
|
273
|
+
then: { required: ['column'] }
|
|
274
|
+
}
|
|
275
|
+
]
|
|
276
|
+
},
|
|
140
277
|
handler: lambda { |args|
|
|
141
278
|
Tools::Tier1.console_aggregate(
|
|
142
279
|
model: args[:model], function: args[:function], column: args[:column], scope: args[:scope]
|
|
@@ -147,12 +284,16 @@ module Woods
|
|
|
147
284
|
name: 'console_association_count',
|
|
148
285
|
description: 'Count associated records for a specific record.',
|
|
149
286
|
properties: {
|
|
150
|
-
model: { type: 'string', description: 'Model name' },
|
|
151
|
-
id: { type: 'integer',
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
287
|
+
model: { type: 'string', pattern: NON_WHITESPACE_SCHEMA_PATTERN, description: 'Model name' },
|
|
288
|
+
id: { type: 'integer', minimum: MIN_INTEGER_INPUT, maximum: MAX_RECORD_ID,
|
|
289
|
+
description: 'Record primary key' },
|
|
290
|
+
association: { type: 'string', pattern: SAFE_IDENTIFIER_SCHEMA_PATTERN,
|
|
291
|
+
description: 'Association name' },
|
|
292
|
+
scope: { type: 'object', propertyNames: { pattern: SCOPE_KEY_SCHEMA_PATTERN },
|
|
293
|
+
patternProperties: SCOPE_VALUE_PATTERN_PROPERTIES,
|
|
294
|
+
description: 'Filter on association: {status: "paid", amount_gt: 0}. ' \
|
|
295
|
+
'Suffixes: _eq _gt _lt _in _null _present. ' \
|
|
296
|
+
'Complex queries: use console_query.' }
|
|
156
297
|
},
|
|
157
298
|
required: %w[model id association],
|
|
158
299
|
tier: 1,
|
|
@@ -166,7 +307,7 @@ module Woods
|
|
|
166
307
|
name: 'console_schema',
|
|
167
308
|
description: 'Get database schema for a model',
|
|
168
309
|
properties: {
|
|
169
|
-
model: { type: 'string', description: 'Model name' },
|
|
310
|
+
model: { type: 'string', pattern: NON_WHITESPACE_SCHEMA_PATTERN, description: 'Model name' },
|
|
170
311
|
include_indexes: { type: 'boolean', description: 'Include index information' }
|
|
171
312
|
},
|
|
172
313
|
required: ['model'],
|
|
@@ -179,14 +320,20 @@ module Woods
|
|
|
179
320
|
name: 'console_recent',
|
|
180
321
|
description: 'Recently created/updated records.',
|
|
181
322
|
properties: {
|
|
182
|
-
model: { type: 'string', description: 'Model name' },
|
|
183
|
-
order_by: { type: 'string',
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
323
|
+
model: { type: 'string', pattern: NON_WHITESPACE_SCHEMA_PATTERN, description: 'Model name' },
|
|
324
|
+
order_by: { type: 'string', pattern: SAFE_IDENTIFIER_SCHEMA_PATTERN,
|
|
325
|
+
description: 'Column to sort by (default: created_at)' },
|
|
326
|
+
direction: { type: 'string', enum: %w[asc desc],
|
|
327
|
+
description: 'Sort direction: asc or desc (default: desc)' },
|
|
328
|
+
limit: { type: 'integer', minimum: MIN_INTEGER_INPUT, maximum: 50,
|
|
329
|
+
description: 'Max records (default 10, max 50)' },
|
|
330
|
+
scope: { type: 'object', propertyNames: { pattern: SCOPE_KEY_SCHEMA_PATTERN },
|
|
331
|
+
patternProperties: SCOPE_VALUE_PATTERN_PROPERTIES,
|
|
332
|
+
description: 'Filter: {status: "paid", total_gt: 0}. ' \
|
|
333
|
+
'Suffixes: _eq _gt _lt _in _null _present. ' \
|
|
334
|
+
'Complex queries: use console_query.' },
|
|
335
|
+
columns: { type: 'array', items: { type: 'string', pattern: SAFE_IDENTIFIER_SCHEMA_PATTERN },
|
|
336
|
+
description: 'Columns to include' }
|
|
190
337
|
},
|
|
191
338
|
required: ['model'],
|
|
192
339
|
tier: 1,
|
|
@@ -214,7 +361,8 @@ module Woods
|
|
|
214
361
|
properties: {
|
|
215
362
|
model: { type: 'string', description: 'Model name' },
|
|
216
363
|
scope: { type: 'object', description: 'Filter conditions' },
|
|
217
|
-
sample_size: { type: 'integer',
|
|
364
|
+
sample_size: { type: 'integer', minimum: MIN_INTEGER_INPUT, maximum: 25,
|
|
365
|
+
description: 'Sample records (default 5, max 25)' }
|
|
218
366
|
},
|
|
219
367
|
required: ['model'],
|
|
220
368
|
tier: 2,
|
|
@@ -229,9 +377,11 @@ module Woods
|
|
|
229
377
|
description: 'Snapshot a record with associations for debugging',
|
|
230
378
|
properties: {
|
|
231
379
|
model: { type: 'string', description: 'Model name' },
|
|
232
|
-
id: { type: 'integer',
|
|
380
|
+
id: { type: 'integer', minimum: MIN_INTEGER_INPUT, maximum: MAX_RECORD_ID,
|
|
381
|
+
description: 'Record primary key' },
|
|
233
382
|
associations: { type: 'array', items: { type: 'string' }, description: 'Association names to include' },
|
|
234
|
-
depth: { type: 'integer',
|
|
383
|
+
depth: { type: 'integer', minimum: MIN_INTEGER_INPUT, maximum: 3,
|
|
384
|
+
description: 'Association depth (default 1, max 3)' }
|
|
235
385
|
},
|
|
236
386
|
required: %w[model id],
|
|
237
387
|
tier: 2,
|
|
@@ -247,7 +397,8 @@ module Woods
|
|
|
247
397
|
description: 'Run validations on an existing record',
|
|
248
398
|
properties: {
|
|
249
399
|
model: { type: 'string', description: 'Model name' },
|
|
250
|
-
id: { type: 'integer',
|
|
400
|
+
id: { type: 'integer', minimum: MIN_INTEGER_INPUT, maximum: MAX_RECORD_ID,
|
|
401
|
+
description: 'Record primary key' },
|
|
251
402
|
attributes: { type: 'object', description: 'Attributes to set before validating' }
|
|
252
403
|
},
|
|
253
404
|
required: %w[model id],
|
|
@@ -290,8 +441,10 @@ module Woods
|
|
|
290
441
|
description: 'Check authorization policy for a record and user',
|
|
291
442
|
properties: {
|
|
292
443
|
model: { type: 'string', description: 'Model name' },
|
|
293
|
-
id: { type: 'integer',
|
|
294
|
-
|
|
444
|
+
id: { type: 'integer', minimum: MIN_INTEGER_INPUT, maximum: MAX_RECORD_ID,
|
|
445
|
+
description: 'Record primary key' },
|
|
446
|
+
user_id: { type: 'integer', minimum: MIN_INTEGER_INPUT, maximum: MAX_RECORD_ID,
|
|
447
|
+
description: 'User to check' },
|
|
295
448
|
action: { type: 'string', description: 'Policy action' }
|
|
296
449
|
},
|
|
297
450
|
required: %w[model id user_id action],
|
|
@@ -323,7 +476,8 @@ module Woods
|
|
|
323
476
|
description: 'Check feature eligibility for a record',
|
|
324
477
|
properties: {
|
|
325
478
|
model: { type: 'string', description: 'Model name' },
|
|
326
|
-
id: { type: 'integer',
|
|
479
|
+
id: { type: 'integer', minimum: MIN_INTEGER_INPUT, maximum: MAX_RECORD_ID,
|
|
480
|
+
description: 'Record primary key' },
|
|
327
481
|
feature: { type: 'string', description: 'Feature name' }
|
|
328
482
|
},
|
|
329
483
|
required: %w[model id feature],
|
|
@@ -339,7 +493,8 @@ module Woods
|
|
|
339
493
|
description: 'Invoke a decorator on a record and return computed attributes',
|
|
340
494
|
properties: {
|
|
341
495
|
model: { type: 'string', description: 'Model name' },
|
|
342
|
-
id: { type: 'integer',
|
|
496
|
+
id: { type: 'integer', minimum: MIN_INTEGER_INPUT, maximum: MAX_RECORD_ID,
|
|
497
|
+
description: 'Record primary key' },
|
|
343
498
|
methods: { type: 'array', items: { type: 'string' }, description: 'Decorator methods to call' }
|
|
344
499
|
},
|
|
345
500
|
required: %w[model id],
|
|
@@ -354,7 +509,8 @@ module Woods
|
|
|
354
509
|
name: 'console_slow_endpoints',
|
|
355
510
|
description: 'List slowest endpoints by response time',
|
|
356
511
|
properties: {
|
|
357
|
-
limit: { type: 'integer',
|
|
512
|
+
limit: { type: 'integer', minimum: MIN_INTEGER_INPUT, maximum: 100,
|
|
513
|
+
description: 'Max endpoints (default 10, max 100)' },
|
|
358
514
|
period: { type: 'string', description: 'Time period (default: 1h)' }
|
|
359
515
|
},
|
|
360
516
|
required: nil,
|
|
@@ -405,7 +561,8 @@ module Woods
|
|
|
405
561
|
name: 'console_job_failures',
|
|
406
562
|
description: 'List recent job failures',
|
|
407
563
|
properties: {
|
|
408
|
-
limit: { type: 'integer',
|
|
564
|
+
limit: { type: 'integer', minimum: MIN_INTEGER_INPUT, maximum: 100,
|
|
565
|
+
description: 'Max failures (default 10, max 100)' },
|
|
409
566
|
queue: { type: 'string', description: 'Filter by queue name' }
|
|
410
567
|
},
|
|
411
568
|
required: nil,
|
|
@@ -429,7 +586,8 @@ module Woods
|
|
|
429
586
|
name: 'console_job_schedule',
|
|
430
587
|
description: 'List scheduled/upcoming jobs',
|
|
431
588
|
properties: {
|
|
432
|
-
limit: { type: 'integer',
|
|
589
|
+
limit: { type: 'integer', minimum: MIN_INTEGER_INPUT, maximum: 100,
|
|
590
|
+
description: 'Max jobs (default 20, max 100)' }
|
|
433
591
|
},
|
|
434
592
|
required: nil,
|
|
435
593
|
tier: 3,
|
|
@@ -480,7 +638,8 @@ module Woods
|
|
|
480
638
|
properties: {
|
|
481
639
|
code: { type: 'string',
|
|
482
640
|
description: 'Ruby code you propose to run (will be surfaced to the user first)' },
|
|
483
|
-
timeout: { type: 'integer',
|
|
641
|
+
timeout: { type: 'integer', minimum: MIN_INTEGER_INPUT, maximum: 30,
|
|
642
|
+
description: 'Timeout in seconds (default 10, max 30)' }
|
|
484
643
|
},
|
|
485
644
|
required: ['code'],
|
|
486
645
|
tier: 4,
|
|
@@ -495,19 +654,28 @@ module Woods
|
|
|
495
654
|
description: [
|
|
496
655
|
'Execute read-only SQL against the live database (SELECT/WITH...SELECT only).',
|
|
497
656
|
'SqlValidator blocks all DML/DDL. Every query runs inside a rolled-back transaction — no writes persist.',
|
|
498
|
-
'Requires embedded_read_tools: true
|
|
657
|
+
'Requires embedded_read_tools: true on the rack middleware, or ' \
|
|
658
|
+
'config.console_embedded_read_tools = true for the stdio server ' \
|
|
659
|
+
'(see docs/CONSOLE_MCP_SETUP.md).',
|
|
499
660
|
'Use console_query instead when you want ActiveRecord query builder rather than raw SQL.'
|
|
500
661
|
].join(' '),
|
|
501
662
|
properties: {
|
|
502
|
-
sql: { type: 'string',
|
|
503
|
-
|
|
663
|
+
sql: { type: 'string', pattern: NON_WHITESPACE_SCHEMA_PATTERN,
|
|
664
|
+
description: 'SQL query (SELECT or WITH...SELECT only)' },
|
|
665
|
+
limit: { type: 'integer', minimum: MIN_INTEGER_INPUT, maximum: 10_000,
|
|
666
|
+
description: 'Max rows returned (default unlimited, max 10000)' }
|
|
504
667
|
},
|
|
505
668
|
required: ['sql'],
|
|
506
669
|
tier: 4,
|
|
507
|
-
handler:
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
670
|
+
# No handler-stage validator: SQL validation is the executor's, which
|
|
671
|
+
# alone knows the host adapter's dialect (CON-2). A `SqlValidator.new`
|
|
672
|
+
# here is the conservative postgres+mysql union, and it ran *first* —
|
|
673
|
+
# so on a MySQL host it rejected statements whose `\'`/backtick
|
|
674
|
+
# grammar produces a spuriously forbidden PostgreSQL view, and the
|
|
675
|
+
# dialect-aware validator at EmbeddedExecutor#handle_sql never got to
|
|
676
|
+
# accept them. No gate is lost: the executor re-validates and raises
|
|
677
|
+
# SqlValidationError, which DispatchPipeline renders as a tool error.
|
|
678
|
+
handler: ->(args) { Tools::Tier4.console_sql(sql: args[:sql], limit: args[:limit]) }
|
|
511
679
|
),
|
|
512
680
|
ToolSpec.new(
|
|
513
681
|
name: 'console_query',
|
|
@@ -516,24 +684,60 @@ module Woods
|
|
|
516
684
|
'Example: {model: "Order", select: ["status", "COUNT(*) AS n"], group_by: ["status"]}.',
|
|
517
685
|
'Use console_count or console_aggregate for simple aggregates without a custom SELECT.',
|
|
518
686
|
'Use console_sql when you need raw SQL that the query builder cannot express.',
|
|
519
|
-
'Requires embedded_read_tools: true
|
|
687
|
+
'Requires embedded_read_tools: true on the rack middleware, or ' \
|
|
688
|
+
'config.console_embedded_read_tools = true for the stdio server ' \
|
|
689
|
+
'(see docs/CONSOLE_MCP_SETUP.md).',
|
|
520
690
|
'Max 10,000 rows returned. Returns columns + rows arrays like a SQL result set.'
|
|
521
691
|
].join(' '),
|
|
522
692
|
properties: {
|
|
523
|
-
model: { type: 'string',
|
|
524
|
-
|
|
693
|
+
model: { type: 'string', pattern: NON_WHITESPACE_SCHEMA_PATTERN,
|
|
694
|
+
description: 'ActiveRecord model name (e.g. "Order")' },
|
|
695
|
+
select: { type: 'array', minItems: 1,
|
|
696
|
+
items: { type: 'string', pattern: SELECT_EXPRESSION_SCHEMA_PATTERN },
|
|
525
697
|
description: 'Columns or expressions to select (e.g. ["status", "COUNT(*) AS n"])' },
|
|
526
|
-
joins: { type: 'array', items: { type: 'string' },
|
|
698
|
+
joins: { type: 'array', items: { type: 'string', pattern: SAFE_IDENTIFIER_SCHEMA_PATTERN },
|
|
527
699
|
description: 'Association names to JOIN (e.g. ["line_items", "user"])' },
|
|
528
|
-
group_by: { type: 'array', items: { type: 'string' },
|
|
700
|
+
group_by: { type: 'array', items: { type: 'string', pattern: COLUMN_REFERENCE_SCHEMA_PATTERN },
|
|
529
701
|
description: 'Columns to GROUP BY (e.g. ["status", "user_id"])' },
|
|
530
|
-
having: { type:
|
|
531
|
-
|
|
702
|
+
having: { type: %w[object array],
|
|
703
|
+
oneOf: [
|
|
704
|
+
{
|
|
705
|
+
type: 'object', minProperties: 1,
|
|
706
|
+
propertyNames: { pattern: COLUMN_REFERENCE_SCHEMA_PATTERN }
|
|
707
|
+
},
|
|
708
|
+
{
|
|
709
|
+
type: 'array', minItems: 2, maxItems: 2,
|
|
710
|
+
# The bind is restricted to scalar JSON types the executor can
|
|
711
|
+
# actually pass to AR's `?` binding: a Hash/Array bind reaches
|
|
712
|
+
# the adapter and fails as a generic execution error instead of
|
|
713
|
+
# a typed validation error (mirrors the `scope:` bind constraint).
|
|
714
|
+
prefixItems: [
|
|
715
|
+
{ type: 'string', pattern: HAVING_TEMPLATE_SCHEMA_PATTERN },
|
|
716
|
+
{ type: %w[string number boolean null] }
|
|
717
|
+
]
|
|
718
|
+
}
|
|
719
|
+
],
|
|
720
|
+
description: 'HAVING condition object or parameterized [template, bind] array' },
|
|
532
721
|
order: { type: 'object',
|
|
722
|
+
propertyNames: { pattern: COLUMN_REFERENCE_SCHEMA_PATTERN },
|
|
723
|
+
additionalProperties: { type: 'string', pattern: ORDER_DIRECTION_SCHEMA_PATTERN },
|
|
533
724
|
description: 'Order specification as {column => direction} (e.g. {"created_at" => "desc"})' },
|
|
534
|
-
scope: { type:
|
|
535
|
-
|
|
536
|
-
|
|
725
|
+
scope: { type: %w[object array],
|
|
726
|
+
oneOf: [
|
|
727
|
+
{ type: 'object', propertyNames: { pattern: SCOPE_KEY_SCHEMA_PATTERN },
|
|
728
|
+
patternProperties: SCOPE_VALUE_PATTERN_PROPERTIES },
|
|
729
|
+
{
|
|
730
|
+
type: 'array', minItems: 2, maxItems: 2,
|
|
731
|
+
prefixItems: [
|
|
732
|
+
{ type: 'string', pattern: QUERY_SCOPE_TEMPLATE_SCHEMA_PATTERN },
|
|
733
|
+
{ type: %w[string number boolean null] }
|
|
734
|
+
]
|
|
735
|
+
}
|
|
736
|
+
],
|
|
737
|
+
description: 'WHERE conditions as {column => value} or exact ' \
|
|
738
|
+
'["column OP ?", bind] array' },
|
|
739
|
+
limit: { type: 'integer', minimum: MIN_INTEGER_INPUT, maximum: 10_000,
|
|
740
|
+
description: 'Maximum rows to return (default 10000, hard max 10000)' }
|
|
537
741
|
},
|
|
538
742
|
required: %w[model select],
|
|
539
743
|
tier: 4,
|
|
@@ -545,8 +749,177 @@ module Woods
|
|
|
545
749
|
)
|
|
546
750
|
}
|
|
547
751
|
)
|
|
548
|
-
].
|
|
752
|
+
].each do |spec|
|
|
753
|
+
spec.wrap_handler! if spec.tier == 1 || %w[console_sql console_query].include?(spec.name)
|
|
754
|
+
end.freeze
|
|
755
|
+
|
|
756
|
+
EXECUTABLE_MODES = {
|
|
757
|
+
embedded: TIER1_TOOLS.map { |name| "console_#{name}" }.freeze,
|
|
758
|
+
embedded_read: (TIER1_TOOLS + %w[sql query]).map { |name| "console_#{name}" }.freeze
|
|
759
|
+
}.freeze
|
|
760
|
+
|
|
761
|
+
REPRESENTATIVE_ARGUMENT_VALUES = {
|
|
762
|
+
model: 'Post', columns: ['id'], id: 1, function: 'count', association: 'comments',
|
|
763
|
+
key: 'feature_flag', value: 'enabled', user_id: 1, action: 'show',
|
|
764
|
+
attributes: { 'title' => 'Example' }, feature: 'preview', job_id: 'job-1',
|
|
765
|
+
code: '1 + 1', sql: 'SELECT 1', select: ['id']
|
|
766
|
+
}.freeze
|
|
767
|
+
|
|
768
|
+
REPRESENTATIVE_VALID_INPUT_OVERRIDES = {
|
|
769
|
+
'console_sample' => { limit: 25, columns: %w[id title] },
|
|
770
|
+
'console_find' => { id: 1, columns: %w[id title] },
|
|
771
|
+
'console_pluck' => { columns: %w[id title] },
|
|
772
|
+
'console_aggregate' => { function: 'sum', column: 'status' },
|
|
773
|
+
'console_schema' => { include_indexes: true },
|
|
774
|
+
'console_recent' => { direction: 'desc', limit: 3, columns: %w[id title] },
|
|
775
|
+
'console_sql' => { sql: 'SELECT id, title FROM posts ORDER BY id' },
|
|
776
|
+
'console_query' => { select: %w[id title], order: { 'id' => 'ASC' } }
|
|
777
|
+
}.transform_values(&:freeze).freeze
|
|
778
|
+
|
|
779
|
+
INVALID_ARGUMENT_VALUES = {
|
|
780
|
+
'string' => [], 'integer' => 'not-an-integer', 'boolean' => 'not-a-boolean',
|
|
781
|
+
'object' => [], 'array' => {}
|
|
782
|
+
}.freeze
|
|
783
|
+
|
|
784
|
+
SEMANTIC_OUTPUT_SHAPES = {
|
|
785
|
+
'console_count' => { count: :integer },
|
|
786
|
+
'console_sample' => { records: :array_of_records },
|
|
787
|
+
'console_find' => { record: :record_or_null },
|
|
788
|
+
'console_pluck' => { columns: :array_of_strings, values: :array },
|
|
789
|
+
'console_aggregate' => { value: :number_or_null },
|
|
790
|
+
'console_association_count' => { count: :integer },
|
|
791
|
+
'console_schema' => { columns: :column_metadata_by_name, indexes: :optional_index_metadata },
|
|
792
|
+
'console_recent' => { records: :array_of_records },
|
|
793
|
+
'console_status' => { status: :string, models: :array_of_strings, adapter: :string },
|
|
794
|
+
'console_diagnose_model' => { diagnostic: :count_recent_and_aggregates },
|
|
795
|
+
'console_data_snapshot' => { record: :record, associations: :records_by_association },
|
|
796
|
+
'console_validate_record' => { valid: :boolean, errors: :validation_errors },
|
|
797
|
+
'console_check_setting' => { setting: :key_value },
|
|
798
|
+
'console_update_setting' => { updated: :boolean, setting: :key_value },
|
|
799
|
+
'console_check_policy' => { allowed: :boolean },
|
|
800
|
+
'console_validate_with' => { valid: :boolean, errors: :validation_errors },
|
|
801
|
+
'console_check_eligibility' => { eligible: :boolean },
|
|
802
|
+
'console_decorate' => { record: :record, computed: :object },
|
|
803
|
+
'console_slow_endpoints' => { endpoints: :performance_rows },
|
|
804
|
+
'console_error_rates' => { error_rates: :rate_rows },
|
|
805
|
+
'console_throughput' => { throughput: :time_series },
|
|
806
|
+
'console_job_queues' => { queues: :queue_statistics },
|
|
807
|
+
'console_job_failures' => { failures: :job_failure_rows },
|
|
808
|
+
'console_job_find' => { job: :job_or_null, retried: :optional_boolean },
|
|
809
|
+
'console_job_schedule' => { jobs: :scheduled_job_rows },
|
|
810
|
+
'console_redis_info' => { info: :redis_section_map },
|
|
811
|
+
'console_cache_stats' => { stats: :cache_statistics },
|
|
812
|
+
'console_channel_status' => { channels: :channel_status_rows },
|
|
813
|
+
'console_eval' => { value: :ruby_value },
|
|
814
|
+
'console_sql' => { columns: :array_of_strings, rows: :array_of_arrays, count: :integer },
|
|
815
|
+
'console_query' => { columns: :array_of_strings, rows: :array_of_arrays, count: :integer }
|
|
816
|
+
}.transform_values(&:freeze).freeze
|
|
817
|
+
|
|
818
|
+
TABLE_GATE_CONTRACTS = {
|
|
819
|
+
'console_count' => :model_before_execution,
|
|
820
|
+
'console_sample' => :model_before_execution,
|
|
821
|
+
'console_find' => :model_before_execution,
|
|
822
|
+
'console_pluck' => :model_before_execution,
|
|
823
|
+
'console_aggregate' => :model_before_execution,
|
|
824
|
+
'console_association_count' => :model_and_association_before_execution,
|
|
825
|
+
'console_schema' => :model_before_execution,
|
|
826
|
+
'console_recent' => :model_before_execution,
|
|
827
|
+
'console_status' => :not_applicable_status_metadata,
|
|
828
|
+
'console_sql' => :sql_before_execution,
|
|
829
|
+
'console_query' => :model_joins_and_rendered_sql_before_execution
|
|
830
|
+
}.freeze
|
|
831
|
+
|
|
832
|
+
REDACTION_CONTRACTS = {
|
|
833
|
+
'console_count' => :not_applicable_scalar_count,
|
|
834
|
+
'console_sample' => :record_columns_and_eav,
|
|
835
|
+
'console_find' => :record_columns_and_eav,
|
|
836
|
+
'console_pluck' => :positional_columns_and_eav,
|
|
837
|
+
'console_aggregate' => :not_applicable_scalar_aggregate,
|
|
838
|
+
'console_association_count' => :not_applicable_scalar_count,
|
|
839
|
+
'console_schema' => :not_applicable_schema_metadata,
|
|
840
|
+
'console_recent' => :record_columns_and_eav,
|
|
841
|
+
'console_status' => :not_applicable_status_metadata,
|
|
842
|
+
'console_sql' => :positional_columns_and_eav,
|
|
843
|
+
'console_query' => :positional_columns_and_eav
|
|
844
|
+
}.freeze
|
|
845
|
+
|
|
846
|
+
TRANSPORT_AUTHORIZATION = {
|
|
847
|
+
stdio: :host_process_access,
|
|
848
|
+
railtie_http: :bearer_token_required
|
|
849
|
+
}.freeze
|
|
850
|
+
|
|
851
|
+
CONFIRMATION_REQUIREMENTS = {
|
|
852
|
+
'console_update_setting' => :required_before_registration,
|
|
853
|
+
'console_job_find' => :required_for_retry_before_registration,
|
|
854
|
+
'console_eval' => :required_before_registration
|
|
855
|
+
}.freeze
|
|
856
|
+
|
|
857
|
+
AUDIT_REQUIREMENTS = {
|
|
858
|
+
'console_eval' => :required_before_registration
|
|
859
|
+
}.freeze
|
|
860
|
+
|
|
861
|
+
# Code-derived inventory of all 31 possible Console tools. Argument
|
|
862
|
+
# names, constraints, and representative schema cases come directly
|
|
863
|
+
# from TOOL_SPECS. Only output semantics and control behavior that cannot
|
|
864
|
+
# be inferred from JSON Schema are declared above.
|
|
865
|
+
# rubocop:disable-next Metrics/BlockLength
|
|
866
|
+
CONTRACT_MATRIX = TOOL_SPECS.map do |spec|
|
|
867
|
+
modes = EXECUTABLE_MODES.filter_map { |mode, names| mode if names.include?(spec.name) }.freeze
|
|
868
|
+
executable = modes.any?
|
|
869
|
+
required = Array(spec.required).freeze
|
|
870
|
+
optional = (spec.properties.keys.map(&:to_s) - required).freeze
|
|
871
|
+
required_input = required.to_h do |name|
|
|
872
|
+
[name.to_sym, REPRESENTATIVE_ARGUMENT_VALUES.fetch(name.to_sym)]
|
|
873
|
+
end
|
|
874
|
+
valid_input = required_input.merge(REPRESENTATIVE_VALID_INPUT_OVERRIDES.fetch(spec.name, {})).freeze
|
|
875
|
+
|
|
876
|
+
invalid_arguments = if required.any?
|
|
877
|
+
valid_input.reject { |name, _value| name == required.first.to_sym }.freeze
|
|
878
|
+
elsif spec.properties.any?
|
|
879
|
+
name, definition = spec.properties.first
|
|
880
|
+
{ name => INVALID_ARGUMENT_VALUES.fetch(Array(definition.fetch(:type)).first) }.freeze
|
|
881
|
+
else
|
|
882
|
+
[].freeze
|
|
883
|
+
end
|
|
884
|
+
error_contract = if required.any?
|
|
885
|
+
{
|
|
886
|
+
stage: :sdk_required_arguments,
|
|
887
|
+
is_error: true,
|
|
888
|
+
text_prefix: "Missing required arguments: #{required.first}"
|
|
889
|
+
}.freeze
|
|
890
|
+
else
|
|
891
|
+
{
|
|
892
|
+
stage: :sdk_schema_validation,
|
|
893
|
+
is_error: true,
|
|
894
|
+
text_prefix: 'Invalid arguments:'
|
|
895
|
+
}.freeze
|
|
896
|
+
end
|
|
897
|
+
|
|
898
|
+
{
|
|
899
|
+
name: spec.name,
|
|
900
|
+
executable_modes: modes,
|
|
901
|
+
arguments: {
|
|
902
|
+
required: required,
|
|
903
|
+
optional: optional,
|
|
904
|
+
constraints: spec.input_schema
|
|
905
|
+
}.freeze,
|
|
906
|
+
representative_valid_input: valid_input,
|
|
907
|
+
representative_invalid_input: {
|
|
908
|
+
arguments: invalid_arguments,
|
|
909
|
+
error_contract: error_contract
|
|
910
|
+
}.freeze,
|
|
911
|
+
semantic_output: {
|
|
912
|
+
availability: executable ? :executable : :inventory_only,
|
|
913
|
+
shape: SEMANTIC_OUTPUT_SHAPES.fetch(spec.name)
|
|
914
|
+
}.freeze,
|
|
915
|
+
authorization: executable ? TRANSPORT_AUTHORIZATION : :not_applicable_unregistered,
|
|
916
|
+
table_gate: executable ? TABLE_GATE_CONTRACTS.fetch(spec.name) : :not_active_unregistered,
|
|
917
|
+
redaction: executable ? REDACTION_CONTRACTS.fetch(spec.name) : :not_active_unregistered,
|
|
918
|
+
credential_scan: executable ? :response_and_error_text_when_enabled : :not_active_unregistered,
|
|
919
|
+
confirmation: CONFIRMATION_REQUIREMENTS.fetch(spec.name, :not_required),
|
|
920
|
+
audit: AUDIT_REQUIREMENTS.fetch(spec.name, executable ? :not_recorded : :not_active_unregistered)
|
|
921
|
+
}.freeze
|
|
922
|
+
end.freeze
|
|
549
923
|
end
|
|
550
924
|
end
|
|
551
925
|
end
|
|
552
|
-
# rubocop:enable Metrics/ModuleLength
|