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
data/lib/woods/console/server.rb
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require 'mcp'
|
|
4
|
+
require_relative '../mcp/protocol_policy'
|
|
4
5
|
require_relative 'connection_manager'
|
|
5
6
|
require_relative 'model_validator'
|
|
6
7
|
require_relative 'safe_context'
|
|
@@ -21,15 +22,24 @@ require_relative 'dispatch_pipeline'
|
|
|
21
22
|
require_relative 'tool_specs'
|
|
22
23
|
|
|
23
24
|
module Woods
|
|
25
|
+
# Same conditional-define pattern used elsewhere in the gem (e.g.
|
|
26
|
+
# Storage::MetadataStore) so this file can be required in isolation
|
|
27
|
+
# without tripping NameError on the raise in Server.build below.
|
|
28
|
+
class Error < StandardError; end unless defined?(Woods::Error)
|
|
29
|
+
class ConfigurationError < Error; end unless defined?(Woods::ConfigurationError)
|
|
30
|
+
|
|
24
31
|
module Console
|
|
25
32
|
# Console MCP Server — queries live Rails application state.
|
|
26
33
|
#
|
|
27
|
-
#
|
|
28
|
-
#
|
|
29
|
-
#
|
|
34
|
+
# Executes against a booted Rails application. The default embedded mode
|
|
35
|
+
# exposes 9 Tier 1 tools; explicit read-tool mode adds console_sql and
|
|
36
|
+
# console_query. The full 31-tool catalogue remains inventory-only.
|
|
30
37
|
#
|
|
31
38
|
# @example
|
|
32
|
-
# server = Woods::Console::Server.
|
|
39
|
+
# server = Woods::Console::Server.build_embedded(
|
|
40
|
+
# model_validator: validator,
|
|
41
|
+
# safe_context: safe_context
|
|
42
|
+
# )
|
|
33
43
|
# transport = MCP::Server::Transports::StdioTransport.new(server)
|
|
34
44
|
# transport.open
|
|
35
45
|
#
|
|
@@ -42,13 +52,8 @@ module Woods
|
|
|
42
52
|
# changes are deployed. The swap is atomic on MRI (GVL) — in-flight scans see
|
|
43
53
|
# either the old or the new index, never a partial one.
|
|
44
54
|
#
|
|
45
|
-
# Returns nil when
|
|
46
|
-
#
|
|
47
|
-
# - No server has been built yet in this process (`build` / `build_embedded`
|
|
48
|
-
# have not been called)
|
|
49
|
-
#
|
|
50
|
-
# Existing callers of `build` / `build_embedded` are unaffected — this is an
|
|
51
|
-
# additive class method with no required arguments beyond `rails_app`.
|
|
55
|
+
# Returns nil when credential defense is disabled or no embedded server
|
|
56
|
+
# has been built yet in this process.
|
|
52
57
|
#
|
|
53
58
|
# @param rails_app [#credentials] The Rails application to re-read.
|
|
54
59
|
# Defaults to `Rails.application` when `Rails` is defined, otherwise
|
|
@@ -73,15 +78,8 @@ module Woods
|
|
|
73
78
|
config&.console_credential_defense_enabled ? true : false
|
|
74
79
|
end
|
|
75
80
|
|
|
76
|
-
# True when
|
|
77
|
-
#
|
|
78
|
-
# `config.console_unsafe_eval_enabled = true`. Explicit config wins
|
|
79
|
-
# over the env var in both directions.
|
|
80
|
-
#
|
|
81
|
-
# NOTE: returning true here does NOT enable eval execution. The
|
|
82
|
-
# execution path is deliberately unimplemented (backlog
|
|
83
|
-
# unsafe-eval-opt-in). This predicate only governs the boot-time
|
|
84
|
-
# banner and the production-environment refusal below.
|
|
81
|
+
# True when legacy configuration requests the unsupported eval path.
|
|
82
|
+
# Explicit config wins over the environment in both directions.
|
|
85
83
|
#
|
|
86
84
|
# @return [Boolean]
|
|
87
85
|
def unsafe_eval_enabled?
|
|
@@ -92,29 +90,13 @@ module Woods
|
|
|
92
90
|
ENV['WOODS_CONSOLE_UNSAFE_EVAL'] == 'true'
|
|
93
91
|
end
|
|
94
92
|
|
|
95
|
-
#
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
# - refuse outright in `Rails.env.production?` (non-negotiable),
|
|
99
|
-
# - otherwise emit a LOUD stderr banner so operators know the flag
|
|
100
|
-
# is live even though eval remains unimplemented.
|
|
101
|
-
#
|
|
102
|
-
# Safe when Rails is not loaded (specs, non-Rails hosts).
|
|
103
|
-
#
|
|
104
|
-
# @return [void]
|
|
105
|
-
# @raise [Woods::ConfigurationError] when the flag is on in production.
|
|
106
|
-
def enforce_unsafe_eval_contract!
|
|
107
|
-
return unless unsafe_eval_enabled?
|
|
93
|
+
# Fail closed because no supported MCP mode registers console_eval.
|
|
94
|
+
def enforce_unsafe_eval_contract!(legacy_options_present: false)
|
|
95
|
+
return unless unsafe_eval_enabled? || legacy_options_present
|
|
108
96
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
'WOODS_CONSOLE_UNSAFE_EVAL is set but Rails.env.production? is true. ' \
|
|
113
|
-
'console_eval cannot be opted into in production. Unset the flag or ' \
|
|
114
|
-
'restart in a non-production environment.'
|
|
115
|
-
end
|
|
116
|
-
|
|
117
|
-
warn unsafe_eval_banner
|
|
97
|
+
raise Woods::ConfigurationError,
|
|
98
|
+
'WOODS_CONSOLE_UNSAFE_EVAL is set, but console_eval is not available in a ' \
|
|
99
|
+
'supported Console MCP mode. Unset the flag; use console_query or console_sql.'
|
|
118
100
|
end
|
|
119
101
|
|
|
120
102
|
# Resolves `Rails.application` when available, else nil.
|
|
@@ -124,36 +106,15 @@ module Woods
|
|
|
124
106
|
Rails.application
|
|
125
107
|
end
|
|
126
108
|
|
|
127
|
-
#
|
|
128
|
-
#
|
|
129
|
-
#
|
|
130
|
-
# The server side of the bridge has no access to the remote app's
|
|
131
|
-
# `ActiveRecord::Base.descendants`, so model_tables and model_reflections
|
|
132
|
-
# are empty. `TableGate#check_sql!` still fires against the raw SQL
|
|
133
|
-
# argument of `console_sql`, but `check_model!`, `check_joins!`, and
|
|
134
|
-
# `check_association!` are effectively no-ops for tools that receive a
|
|
135
|
-
# model name rather than SQL (find, sample, count, etc.). A bridge-mode
|
|
136
|
-
# deployment therefore relies on Layer 2 (credential scanning) + Layer 3
|
|
137
|
-
# (column/EAV redaction) + Layer 4 (SqlValidator + SafeContext rollback)
|
|
138
|
-
# for non-SQL tool calls. If you need full Layer 1 coverage, use
|
|
139
|
-
# `build_embedded` (the stdio entry point `exe/woods-console` does this).
|
|
140
|
-
#
|
|
141
|
-
# See docs/CONSOLE_MCP_SETUP.md "Bridge vs. embedded defense coverage"
|
|
142
|
-
# for the full matrix.
|
|
143
|
-
#
|
|
109
|
+
# The former JSON-lines bridge never executed live queries. Keep this
|
|
110
|
+
# entry point fail-closed so legacy callers cannot receive fabricated
|
|
111
|
+
# empty responses.
|
|
144
112
|
# @param config [Hash] Configuration hash (from YAML or env)
|
|
145
|
-
# @
|
|
146
|
-
def build(config:)
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
redacted_key_values = Array(
|
|
151
|
-
config['redacted_key_values'] || connection_config['redacted_key_values']
|
|
152
|
-
)
|
|
153
|
-
safe_ctx = build_safe_context(redacted_columns, redacted_key_values)
|
|
154
|
-
ctx = build_response_context(safe_ctx: safe_ctx, model_tables: {}, model_reflections: {})
|
|
155
|
-
|
|
156
|
-
build_server(conn_mgr, ctx)
|
|
113
|
+
# @raise [Woods::ConfigurationError] always
|
|
114
|
+
def build(config:) # rubocop:disable Lint/UnusedMethodArgument
|
|
115
|
+
raise Woods::ConfigurationError,
|
|
116
|
+
'The JSON-lines Console bridge is not supported. Use Server.build_embedded, ' \
|
|
117
|
+
'rake woods:console, or Woods::Console::RackMiddleware.'
|
|
157
118
|
end
|
|
158
119
|
|
|
159
120
|
# Build a configured MCP::Server using embedded ActiveRecord execution.
|
|
@@ -168,11 +129,9 @@ module Woods
|
|
|
168
129
|
# {key_column:, value_column:, sensitive_keys: []}. See SafeContext for semantics.
|
|
169
130
|
# @param connection [Object, nil] Database connection for adapter detection
|
|
170
131
|
# @param read_tools_enabled [Boolean] Enable sql/query tools in embedded mode (default: false)
|
|
171
|
-
# @param unsafe_eval_confirmation [Confirmation, nil]
|
|
172
|
-
#
|
|
173
|
-
#
|
|
174
|
-
# @param unsafe_eval_audit_log_path [String, Pathname, nil] JSONL audit log
|
|
175
|
-
# path for `console_eval`. Required when the opt-in is on.
|
|
132
|
+
# @param unsafe_eval_confirmation [Confirmation, nil] Retained for API compatibility;
|
|
133
|
+
# no supported mode registers console_eval.
|
|
134
|
+
# @param unsafe_eval_audit_log_path [String, Pathname, nil] Retained for API compatibility.
|
|
176
135
|
# @return [MCP::Server] Configured server ready for transport
|
|
177
136
|
def build_embedded(model_validator:, safe_context:, redacted_columns: [], # rubocop:disable Metrics/ParameterLists
|
|
178
137
|
redacted_key_values: [], connection: nil,
|
|
@@ -181,142 +140,32 @@ module Woods
|
|
|
181
140
|
unsafe_eval_confirmation: nil,
|
|
182
141
|
unsafe_eval_audit_log_path: nil)
|
|
183
142
|
require_relative 'embedded_executor'
|
|
184
|
-
enforce_unsafe_eval_contract!
|
|
143
|
+
enforce_unsafe_eval_contract!(
|
|
144
|
+
legacy_options_present: unsafe_eval_confirmation || unsafe_eval_audit_log_path
|
|
145
|
+
)
|
|
185
146
|
|
|
186
|
-
safe_ctx =
|
|
187
|
-
ctx = build_response_context(safe_ctx:
|
|
147
|
+
safe_ctx, render_ctx = policy_contexts(safe_context, redacted_columns, redacted_key_values)
|
|
148
|
+
ctx = build_response_context(safe_ctx: render_ctx, model_tables: model_tables,
|
|
188
149
|
model_reflections: model_reflections)
|
|
189
150
|
|
|
190
|
-
eval_wiring = build_unsafe_eval_wiring(
|
|
191
|
-
confirmation: unsafe_eval_confirmation,
|
|
192
|
-
audit_log_path: unsafe_eval_audit_log_path
|
|
193
|
-
)
|
|
194
|
-
|
|
195
151
|
# Wire the same TableGate into the executor so sql/query are blocked
|
|
196
152
|
# PRE-execution against console_blocked_tables (previously TableGate
|
|
197
153
|
# was only consulted on the render path, leaving the defense inert
|
|
198
154
|
# for the sql and query tools).
|
|
199
|
-
table_gate = ctx&.table_gate
|
|
200
155
|
executor = EmbeddedExecutor.new(
|
|
201
|
-
model_validator: model_validator, safe_context:
|
|
156
|
+
model_validator: model_validator, safe_context: safe_ctx,
|
|
202
157
|
connection: connection, read_tools_enabled: read_tools_enabled,
|
|
203
|
-
table_gate: table_gate
|
|
204
|
-
eval_guard: eval_wiring[:eval_guard],
|
|
205
|
-
confirmation: eval_wiring[:confirmation],
|
|
206
|
-
audit_logger: eval_wiring[:audit_logger],
|
|
207
|
-
unsafe_eval_enabled: eval_wiring[:unsafe_eval_enabled]
|
|
158
|
+
table_gate: ctx&.table_gate
|
|
208
159
|
)
|
|
209
160
|
|
|
210
|
-
|
|
161
|
+
mode = read_tools_enabled ? :embedded_read : :embedded
|
|
162
|
+
build_server(executor, ctx, tool_names: executable_tool_names(mode))
|
|
211
163
|
end
|
|
212
164
|
|
|
213
|
-
# Resolve the
|
|
214
|
-
#
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
# EvalGuard is never reached.
|
|
218
|
-
#
|
|
219
|
-
# When it's true, BOTH a {Confirmation} and an audit-log path MUST be
|
|
220
|
-
# provided (via kwargs or config); otherwise we raise so a
|
|
221
|
-
# misconfigured host fails at boot instead of silently running Ruby
|
|
222
|
-
# without approval or audit. See backlog B-053.
|
|
223
|
-
#
|
|
224
|
-
# @param confirmation [Confirmation, nil] Explicit kwarg wins over
|
|
225
|
-
# `config.console_unsafe_eval_confirmation`.
|
|
226
|
-
# @param audit_log_path [String, Pathname, nil] Explicit kwarg wins
|
|
227
|
-
# over `config.console_unsafe_eval_audit_log_path`.
|
|
228
|
-
# @return [Hash] { eval_guard:, confirmation:, audit_logger:, unsafe_eval_enabled: }
|
|
229
|
-
# @raise [Woods::ConfigurationError] when opt-in is on but either
|
|
230
|
-
# collaborator is missing.
|
|
231
|
-
def build_unsafe_eval_wiring(confirmation:, audit_log_path:)
|
|
232
|
-
return empty_unsafe_eval_wiring unless unsafe_eval_enabled?
|
|
233
|
-
|
|
234
|
-
config = Woods.configuration if Woods.respond_to?(:configuration)
|
|
235
|
-
confirmation ||= config&.console_unsafe_eval_confirmation
|
|
236
|
-
audit_log_path ||= config&.console_unsafe_eval_audit_log_path
|
|
237
|
-
require_unsafe_eval_collaborators!(confirmation, audit_log_path)
|
|
238
|
-
|
|
239
|
-
{
|
|
240
|
-
eval_guard: EvalGuard.new,
|
|
241
|
-
confirmation: confirmation,
|
|
242
|
-
audit_logger: AuditLogger.new(path: audit_log_path.to_s),
|
|
243
|
-
unsafe_eval_enabled: true
|
|
244
|
-
}
|
|
245
|
-
end
|
|
246
|
-
|
|
247
|
-
def empty_unsafe_eval_wiring
|
|
248
|
-
{ eval_guard: nil, confirmation: nil, audit_logger: nil, unsafe_eval_enabled: false }
|
|
249
|
-
end
|
|
250
|
-
|
|
251
|
-
def require_unsafe_eval_collaborators!(confirmation, audit_log_path)
|
|
252
|
-
if confirmation.nil?
|
|
253
|
-
raise Woods::ConfigurationError,
|
|
254
|
-
'WOODS_CONSOLE_UNSAFE_EVAL is set but no Confirmation was provided. ' \
|
|
255
|
-
'Pass `unsafe_eval_confirmation:` to Server.build_embedded / RackMiddleware ' \
|
|
256
|
-
'or set `config.console_unsafe_eval_confirmation`. Fail-closed by design — ' \
|
|
257
|
-
'see backlog B-053 / docs/CONSOLE_MCP_SETUP.md.'
|
|
258
|
-
end
|
|
259
|
-
return unless audit_log_path.nil? || audit_log_path.to_s.strip.empty?
|
|
260
|
-
|
|
261
|
-
raise Woods::ConfigurationError,
|
|
262
|
-
'WOODS_CONSOLE_UNSAFE_EVAL is set but no audit-log path was provided. ' \
|
|
263
|
-
'Pass `unsafe_eval_audit_log_path:` to Server.build_embedded / RackMiddleware ' \
|
|
264
|
-
'or set `config.console_unsafe_eval_audit_log_path`. Every console_eval run ' \
|
|
265
|
-
'must be audited.'
|
|
266
|
-
end
|
|
267
|
-
|
|
268
|
-
# Register all tool specs for a given tier on the server.
|
|
269
|
-
#
|
|
270
|
-
# @param server [MCP::Server] The MCP server instance
|
|
271
|
-
# @param conn_mgr [ConnectionManager, EmbeddedExecutor] Request executor
|
|
272
|
-
# @param ctx [ResponseContext, nil] Optional context bundling response-safety layers
|
|
273
|
-
# @param tier [Integer] Tier number (1-4)
|
|
274
|
-
# @param renderer [ConsoleResponseRenderer, nil] Optional response renderer
|
|
275
|
-
# @return [void]
|
|
276
|
-
def register_tier_tools(server, conn_mgr, ctx, tier:, renderer: nil)
|
|
277
|
-
TOOL_SPECS.select { |spec| spec.tier == tier }.each do |spec|
|
|
278
|
-
register(spec, server, conn_mgr, ctx, renderer: renderer)
|
|
279
|
-
end
|
|
280
|
-
end
|
|
281
|
-
|
|
282
|
-
# Register Tier 1 read-only tools on the server.
|
|
283
|
-
#
|
|
284
|
-
# @param server [MCP::Server] The MCP server instance
|
|
285
|
-
# @param conn_mgr [ConnectionManager, EmbeddedExecutor] Request executor
|
|
286
|
-
# @param ctx [ResponseContext, nil] Optional context for column redaction
|
|
287
|
-
# @return [void]
|
|
288
|
-
def register_tier1_tools(server, conn_mgr, ctx = nil, renderer: nil)
|
|
289
|
-
register_tier_tools(server, conn_mgr, ctx, tier: 1, renderer: renderer)
|
|
290
|
-
end
|
|
291
|
-
|
|
292
|
-
# Register Tier 2 domain-aware tools on the server.
|
|
293
|
-
#
|
|
294
|
-
# @param server [MCP::Server] The MCP server instance
|
|
295
|
-
# @param conn_mgr [ConnectionManager, EmbeddedExecutor] Request executor
|
|
296
|
-
# @param ctx [ResponseContext, nil] Optional context for column redaction
|
|
297
|
-
# @return [void]
|
|
298
|
-
def register_tier2_tools(server, conn_mgr, ctx = nil, renderer: nil)
|
|
299
|
-
register_tier_tools(server, conn_mgr, ctx, tier: 2, renderer: renderer)
|
|
300
|
-
end
|
|
301
|
-
|
|
302
|
-
# Register Tier 3 analytics tools on the server.
|
|
303
|
-
#
|
|
304
|
-
# @param server [MCP::Server] The MCP server instance
|
|
305
|
-
# @param conn_mgr [ConnectionManager, EmbeddedExecutor] Request executor
|
|
306
|
-
# @param ctx [ResponseContext, nil] Optional context for column redaction
|
|
307
|
-
# @return [void]
|
|
308
|
-
def register_tier3_tools(server, conn_mgr, ctx = nil, renderer: nil)
|
|
309
|
-
register_tier_tools(server, conn_mgr, ctx, tier: 3, renderer: renderer)
|
|
310
|
-
end
|
|
311
|
-
|
|
312
|
-
# Register Tier 4 guarded tools on the server.
|
|
313
|
-
#
|
|
314
|
-
# @param server [MCP::Server] The MCP server instance
|
|
315
|
-
# @param conn_mgr [ConnectionManager, EmbeddedExecutor] Request executor
|
|
316
|
-
# @param ctx [ResponseContext, nil] Optional context for column redaction
|
|
317
|
-
# @return [void]
|
|
318
|
-
def register_tier4_tools(server, conn_mgr, ctx = nil, renderer: nil)
|
|
319
|
-
register_tier_tools(server, conn_mgr, ctx, tier: 4, renderer: renderer)
|
|
165
|
+
# Resolve the exact tool list for a supported embedded mode from the
|
|
166
|
+
# same matrix used by tests and documentation evidence.
|
|
167
|
+
def executable_tool_names(mode)
|
|
168
|
+
CONTRACT_MATRIX.filter_map { |row| row[:name] if row[:executable_modes].include?(mode) }
|
|
320
169
|
end
|
|
321
170
|
|
|
322
171
|
private
|
|
@@ -337,7 +186,7 @@ module Woods
|
|
|
337
186
|
pipeline = DispatchPipeline.new(
|
|
338
187
|
tool_name: spec.name,
|
|
339
188
|
handler: spec.handler,
|
|
340
|
-
|
|
189
|
+
properties: spec.properties,
|
|
341
190
|
conn_mgr: conn_mgr,
|
|
342
191
|
ctx: ctx || NullResponseContext.instance,
|
|
343
192
|
renderer: renderer,
|
|
@@ -355,32 +204,72 @@ module Woods
|
|
|
355
204
|
# @param spec [ToolSpec]
|
|
356
205
|
# @return [Hash]
|
|
357
206
|
def spec_schema(spec)
|
|
358
|
-
|
|
359
|
-
schema[:required] = spec.required if spec.required&.any?
|
|
360
|
-
schema
|
|
207
|
+
spec.input_schema
|
|
361
208
|
end
|
|
362
209
|
|
|
363
|
-
#
|
|
364
|
-
#
|
|
365
|
-
#
|
|
366
|
-
#
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
#
|
|
372
|
-
#
|
|
210
|
+
# Build the SafeContext (Layer 3) pair the embedded server runs on:
|
|
211
|
+
# [executor context, render context].
|
|
212
|
+
#
|
|
213
|
+
# Audit finding B1: the transport-provided SafeContext owns the
|
|
214
|
+
# connection/pool, the statement timeout, and the rolled-back
|
|
215
|
+
# transaction. Building a second, render-only SafeContext for the
|
|
216
|
+
# redaction lists left the executor's context without the policy, so
|
|
217
|
+
# every executor-side redaction-oracle refusal was dead on the real
|
|
218
|
+
# transports. Whenever redaction is EFFECTIVELY configured — the
|
|
219
|
+
# kwargs when either is supplied, otherwise the lists the supplied
|
|
220
|
+
# context itself carries — derive ONE policy-complete context from
|
|
221
|
+
# the transport context ({SafeContext#with_redaction_policy} — same
|
|
222
|
+
# pool/timeout/transaction behavior, plus the effective lists) and
|
|
223
|
+
# hand that single context to both the executor and the response
|
|
224
|
+
# renderer. The executor refuses oracle shapes; the renderer masks
|
|
225
|
+
# the direct unaliased selections that remain permitted.
|
|
226
|
+
#
|
|
227
|
+
# Fails closed ({Woods::ConfigurationError}) when redaction is
|
|
228
|
+
# effectively configured but the supplied object cannot derive a
|
|
229
|
+
# policy-complete context: the previous silent split wiring left the
|
|
230
|
+
# renderer disabled while the executor still ran the policy. When
|
|
231
|
+
# nothing is effectively configured — no kwargs, and a context that
|
|
232
|
+
# cannot be inspected — the caller's context passes through
|
|
233
|
+
# untouched and the render context stays nil (NullResponseContext).
|
|
234
|
+
#
|
|
235
|
+
# @param safe_context [SafeContext, nil] Transport-provided Layer 3
|
|
373
236
|
# @param redacted_columns [Array<String>]
|
|
374
237
|
# @param redacted_key_values [Array<Hash>]
|
|
375
|
-
# @return [SafeContext, nil]
|
|
376
|
-
def
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
238
|
+
# @return [Array(SafeContext, SafeContext, nil)] executor and render contexts
|
|
239
|
+
def policy_contexts(safe_context, redacted_columns, redacted_key_values)
|
|
240
|
+
configured_columns, configured_key_values = effective_policy(
|
|
241
|
+
safe_context, redacted_columns, redacted_key_values
|
|
242
|
+
)
|
|
243
|
+
return [safe_context, nil] unless configured_columns.any? || configured_key_values.any?
|
|
244
|
+
unless safe_context.respond_to?(:with_redaction_policy)
|
|
245
|
+
raise Woods::ConfigurationError,
|
|
246
|
+
'Console redaction is configured, but the supplied SafeContext cannot derive a ' \
|
|
247
|
+
'policy-complete context (SafeContext#with_redaction_policy). Construction fails ' \
|
|
248
|
+
'closed: the executor would run with the policy while the renderer stays disabled.'
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
derived = safe_context.with_redaction_policy(
|
|
252
|
+
redacted_columns: configured_columns,
|
|
253
|
+
redacted_key_values: configured_key_values
|
|
383
254
|
)
|
|
255
|
+
[derived, derived]
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
# The effective redaction policy: the kwargs when either is supplied
|
|
259
|
+
# (the documented configuration surface), otherwise the lists the
|
|
260
|
+
# supplied context itself carries. Returns empty lists when no kwargs
|
|
261
|
+
# were supplied and the context cannot be inspected (nil, or a
|
|
262
|
+
# duck-typed object without the redacted_* readers) — nothing is
|
|
263
|
+
# effectively configured, so construction proceeds with the caller's
|
|
264
|
+
# context untouched.
|
|
265
|
+
#
|
|
266
|
+
# @return [Array(Array, Array)] [columns, key_values]
|
|
267
|
+
def effective_policy(safe_context, redacted_columns, redacted_key_values)
|
|
268
|
+
return [redacted_columns, redacted_key_values] if redacted_columns.any? || redacted_key_values.any?
|
|
269
|
+
return [[], []] unless safe_context.respond_to?(:redacted_columns) &&
|
|
270
|
+
safe_context.respond_to?(:redacted_key_values)
|
|
271
|
+
|
|
272
|
+
[Array(safe_context.redacted_columns), Array(safe_context.redacted_key_values)]
|
|
384
273
|
end
|
|
385
274
|
|
|
386
275
|
# Bundle the three response-safety layers into a ResponseContext the
|
|
@@ -466,40 +355,23 @@ module Woods
|
|
|
466
355
|
|
|
467
356
|
# Shared server construction used by both build() and build_embedded().
|
|
468
357
|
#
|
|
469
|
-
# @param conn_mgr [
|
|
358
|
+
# @param conn_mgr [EmbeddedExecutor] Request executor
|
|
470
359
|
# @param ctx [ResponseContext, nil] Optional context bundling response-safety layers
|
|
360
|
+
# @param tool_names [Array<String>] Exact executable tool names to advertise
|
|
471
361
|
# @return [MCP::Server]
|
|
472
|
-
def build_server(conn_mgr, ctx)
|
|
362
|
+
def build_server(conn_mgr, ctx, tool_names:)
|
|
473
363
|
server = ::MCP::Server.new(
|
|
474
364
|
name: 'woods-console',
|
|
475
|
-
version: defined?(Woods::VERSION) ? Woods::VERSION : '0.1.0'
|
|
365
|
+
version: defined?(Woods::VERSION) ? Woods::VERSION : '0.1.0',
|
|
366
|
+
**Woods::MCP::ProtocolPolicy.cache_hints
|
|
476
367
|
)
|
|
477
368
|
|
|
478
369
|
renderer = build_console_renderer
|
|
479
370
|
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
server
|
|
485
|
-
end
|
|
486
|
-
|
|
487
|
-
# Loud multi-line banner surfaced to stderr when the opt-in flag is
|
|
488
|
-
# recognised outside of production. Operators should see this every
|
|
489
|
-
# boot so an accidentally-persistent env var cannot go unnoticed.
|
|
490
|
-
#
|
|
491
|
-
# @return [String]
|
|
492
|
-
def unsafe_eval_banner
|
|
493
|
-
<<~BANNER
|
|
494
|
-
|
|
495
|
-
================================================================================
|
|
496
|
-
WOODS_CONSOLE_UNSAFE_EVAL IS SET
|
|
497
|
-
console_eval is LIVE on this process. Every run goes through EvalGuard +
|
|
498
|
-
Confirmation + SafeContext rollback + a wall-clock timeout and is recorded
|
|
499
|
-
to the audit log. The flag refuses to boot in Rails.env.production?.
|
|
500
|
-
If you did not mean to set this, unset the env var.
|
|
501
|
-
================================================================================
|
|
502
|
-
BANNER
|
|
371
|
+
TOOL_SPECS.select { |spec| tool_names.include?(spec.name) }.each do |spec|
|
|
372
|
+
register(spec, server, conn_mgr, ctx, renderer: renderer)
|
|
373
|
+
end
|
|
374
|
+
Woods::MCP::ProtocolPolicy.sort_tools!(server)
|
|
503
375
|
end
|
|
504
376
|
|
|
505
377
|
def structured_logger
|