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.
Files changed (274) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +2035 -0
  3. data/CONTRIBUTING.md +253 -87
  4. data/README.md +161 -513
  5. data/SECURITY.md +92 -0
  6. data/assets/woods-wordmark-white-with-bg.png +0 -0
  7. data/docs/AGENT_GUIDE.md +204 -0
  8. data/docs/AGENT_SETUP.md +205 -0
  9. data/docs/BACKEND_MATRIX.md +470 -0
  10. data/docs/CONFIGURATION_REFERENCE.md +655 -0
  11. data/docs/CONSOLE_MCP_SETUP.md +829 -0
  12. data/docs/DOCKER_SETUP.md +454 -0
  13. data/docs/EMBEDDING_MODELS.md +136 -0
  14. data/docs/EVALUATION.md +91 -0
  15. data/docs/EXTRACTOR_REFERENCE.md +765 -0
  16. data/docs/FAQ.md +544 -0
  17. data/docs/GETTING_STARTED.md +183 -0
  18. data/docs/INCREMENTAL_EXTRACTION.md +455 -0
  19. data/docs/INTERNALS.md +418 -0
  20. data/docs/MCP_HTTP_TRANSPORT.md +144 -0
  21. data/docs/MCP_SERVERS.md +231 -0
  22. data/docs/MCP_TOOL_COOKBOOK.md +987 -0
  23. data/docs/MCP_WORKTREE_SETUP.md +127 -0
  24. data/docs/NOTION_INTEGRATION.md +283 -0
  25. data/docs/OBSIDIAN_INTEGRATION.md +170 -0
  26. data/docs/PUBLISHED_INDEX.md +213 -0
  27. data/docs/README.md +94 -0
  28. data/docs/RETRIEVAL_GUIDE.md +267 -0
  29. data/docs/TOKEN_BENCHMARK.md +68 -0
  30. data/docs/TROUBLESHOOTING.md +841 -0
  31. data/docs/UNBLOCKED_INTEGRATION.md +279 -0
  32. data/docs/UPGRADING_TO_2.md +321 -0
  33. data/docs/WATCH_DAEMON.md +667 -0
  34. data/docs/WHY_WOODS.md +219 -0
  35. data/exe/woods-console +40 -4
  36. data/exe/woods-console-mcp +21 -35
  37. data/exe/woods-mcp +20 -7
  38. data/exe/woods-mcp-http +80 -11
  39. data/exe/woods-mcp-start +57 -52
  40. data/lib/generators/woods/install_generator.rb +6 -5
  41. data/lib/generators/woods/pgvector_generator.rb +6 -3
  42. data/lib/generators/woods/templates/add_pgvector_to_woods.rb.erb +29 -9
  43. data/lib/generators/woods/templates/create_woods_tables.rb.erb +5 -1
  44. data/lib/generators/woods/templates/woods.rb.tt +49 -28
  45. data/lib/tasks/woods.rake +622 -168
  46. data/lib/tasks/woods_checks.rake +107 -0
  47. data/lib/tasks/woods_evaluation.rake +164 -80
  48. data/lib/woods/ast/call_site_extractor.rb +6 -15
  49. data/lib/woods/ast/method_extractor.rb +19 -9
  50. data/lib/woods/ast/parser.rb +54 -8
  51. data/lib/woods/atomic_file.rb +171 -2
  52. data/lib/woods/builder.rb +310 -22
  53. data/lib/woods/cache/cache_middleware.rb +7 -2
  54. data/lib/woods/cache/cache_store.rb +9 -1
  55. data/lib/woods/cache/solid_cache_store.rb +6 -4
  56. data/lib/woods/change_set.rb +88 -0
  57. data/lib/woods/checks/generation_resolution.rb +34 -0
  58. data/lib/woods/checks/moved_messages.rb +186 -0
  59. data/lib/woods/chunking/semantic_chunker.rb +160 -18
  60. data/lib/woods/console/audit_logger.rb +12 -3
  61. data/lib/woods/console/bridge_protocol.rb +3 -16
  62. data/lib/woods/console/connection_manager.rb +51 -136
  63. data/lib/woods/console/dispatch_pipeline.rb +42 -12
  64. data/lib/woods/console/embedded_executor.rb +806 -149
  65. data/lib/woods/console/eval_guard.rb +27 -20
  66. data/lib/woods/console/input_contract.rb +78 -0
  67. data/lib/woods/console/model_validator.rb +29 -1
  68. data/lib/woods/console/rack_middleware.rb +65 -42
  69. data/lib/woods/console/redactor.rb +26 -8
  70. data/lib/woods/console/safe_context.rb +58 -10
  71. data/lib/woods/console/scope_predicate_parser.rb +41 -0
  72. data/lib/woods/console/server.rb +119 -247
  73. data/lib/woods/console/sql_noise_stripper.rb +125 -16
  74. data/lib/woods/console/sql_table_scanner.rb +82 -22
  75. data/lib/woods/console/sql_validator.rb +459 -29
  76. data/lib/woods/console/table_gate.rb +2 -2
  77. data/lib/woods/console/tool_specs.rb +463 -90
  78. data/lib/woods/console/tools/tier1.rb +1 -5
  79. data/lib/woods/console/tools/tier4.rb +18 -9
  80. data/lib/woods/coordination/lock_heartbeat.rb +103 -0
  81. data/lib/woods/coordination/pipeline_lock.rb +263 -53
  82. data/lib/woods/db/migrations/007_typed_snapshot_units.rb +45 -0
  83. data/lib/woods/db/migrator.rb +3 -9
  84. data/lib/woods/db/schema_version.rb +47 -2
  85. data/lib/woods/dependency_graph.rb +898 -64
  86. data/lib/woods/embedding/fake.rb +138 -0
  87. data/lib/woods/embedding/indexer.rb +832 -40
  88. data/lib/woods/embedding/openai.rb +77 -19
  89. data/lib/woods/embedding/provider.rb +189 -11
  90. data/lib/woods/embedding/text_preparer.rb +1 -1
  91. data/lib/woods/embedding/token_counter.rb +0 -7
  92. data/lib/woods/evaluation/ablation_agent_payload.rb +38 -0
  93. data/lib/woods/evaluation/ablation_executor.rb +67 -0
  94. data/lib/woods/evaluation/ablation_provenance.rb +38 -0
  95. data/lib/woods/evaluation/ablation_report_writer.rb +43 -0
  96. data/lib/woods/evaluation/ablation_runner.rb +173 -0
  97. data/lib/woods/evaluation/ablation_summary.rb +65 -0
  98. data/lib/woods/evaluation/ablation_task.rb +66 -0
  99. data/lib/woods/evaluation/ablation_task_set.rb +77 -0
  100. data/lib/woods/evaluation/ablation_timed_executor.rb +91 -0
  101. data/lib/woods/evaluation/ablation_worktree.rb +71 -0
  102. data/lib/woods/evaluation/baseline.rb +60 -0
  103. data/lib/woods/evaluation/baseline_runner.rb +11 -3
  104. data/lib/woods/evaluation/evaluator.rb +41 -8
  105. data/lib/woods/evaluation/query_set.rb +79 -13
  106. data/lib/woods/evaluation/report_generator.rb +20 -1
  107. data/lib/woods/export/unit_facts.rb +0 -11
  108. data/lib/woods/extracted_unit.rb +22 -63
  109. data/lib/woods/extractor.rb +2783 -238
  110. data/lib/woods/extractors/action_cable_extractor.rb +9 -4
  111. data/lib/woods/extractors/ast_source_extraction.rb +20 -2
  112. data/lib/woods/extractors/caching_extractor.rb +46 -12
  113. data/lib/woods/extractors/callback_analyzer.rb +39 -9
  114. data/lib/woods/extractors/component_discovery.rb +123 -0
  115. data/lib/woods/extractors/concern_extractor.rb +17 -3
  116. data/lib/woods/extractors/controller_extractor.rb +389 -29
  117. data/lib/woods/extractors/decorator_extractor.rb +7 -14
  118. data/lib/woods/extractors/engine_extractor.rb +53 -8
  119. data/lib/woods/extractors/event_extractor.rb +55 -4
  120. data/lib/woods/extractors/factory_extractor.rb +49 -11
  121. data/lib/woods/extractors/graphql_extractor.rb +162 -66
  122. data/lib/woods/extractors/i18n_extractor.rb +6 -1
  123. data/lib/woods/extractors/job_extractor.rb +51 -21
  124. data/lib/woods/extractors/lib_extractor.rb +23 -17
  125. data/lib/woods/extractors/line_neutralizer.rb +171 -0
  126. data/lib/woods/extractors/mailer_extractor.rb +9 -1
  127. data/lib/woods/extractors/manager_extractor.rb +19 -2
  128. data/lib/woods/extractors/migration_extractor.rb +22 -11
  129. data/lib/woods/extractors/model_extractor.rb +292 -57
  130. data/lib/woods/extractors/package_extractor.rb +154 -0
  131. data/lib/woods/extractors/phlex_extractor.rb +18 -3
  132. data/lib/woods/extractors/policy_extractor.rb +6 -5
  133. data/lib/woods/extractors/poro_extractor.rb +13 -14
  134. data/lib/woods/extractors/pundit_extractor.rb +3 -3
  135. data/lib/woods/extractors/rails_source_extractor.rb +24 -7
  136. data/lib/woods/extractors/rake_task_extractor.rb +158 -30
  137. data/lib/woods/extractors/reference_patterns.rb +38 -0
  138. data/lib/woods/extractors/route_extractor.rb +58 -2
  139. data/lib/woods/extractors/scheduled_job_extractor.rb +51 -35
  140. data/lib/woods/extractors/serializer_extractor.rb +3 -4
  141. data/lib/woods/extractors/service_extractor.rb +11 -1
  142. data/lib/woods/extractors/shared_dependency_scanner.rb +24 -34
  143. data/lib/woods/extractors/shared_utility_methods.rb +36 -6
  144. data/lib/woods/extractors/source_nesting.rb +560 -0
  145. data/lib/woods/extractors/state_machine_extractor.rb +30 -18
  146. data/lib/woods/extractors/test_mapping_extractor.rb +26 -9
  147. data/lib/woods/extractors/view_component_extractor.rb +28 -3
  148. data/lib/woods/extractors/view_engines/erb.rb +17 -3
  149. data/lib/woods/feedback/gap_detector.rb +9 -3
  150. data/lib/woods/feedback/store.rb +7 -1
  151. data/lib/woods/filename_utils.rb +29 -1
  152. data/lib/woods/flow_analysis/operation_extractor.rb +22 -10
  153. data/lib/woods/flow_assembler.rb +147 -26
  154. data/lib/woods/flow_document.rb +1 -0
  155. data/lib/woods/flow_precomputer.rb +175 -22
  156. data/lib/woods/gem_mapper.rb +285 -0
  157. data/lib/woods/generation.rb +185 -0
  158. data/lib/woods/git_command.rb +38 -0
  159. data/lib/woods/git_provenance.rb +16 -2
  160. data/lib/woods/graph_analyzer.rb +564 -87
  161. data/lib/woods/index_artifact.rb +93 -23
  162. data/lib/woods/mcp/bearer_auth.rb +102 -13
  163. data/lib/woods/mcp/bootstrap_state.rb +77 -0
  164. data/lib/woods/mcp/bootstrapper.rb +582 -77
  165. data/lib/woods/mcp/config_resolver.rb +66 -6
  166. data/lib/woods/mcp/errors.rb +60 -0
  167. data/lib/woods/mcp/index_reader.rb +836 -117
  168. data/lib/woods/mcp/index_reader_pinning.rb +78 -0
  169. data/lib/woods/mcp/origin_guard.rb +66 -7
  170. data/lib/woods/mcp/protocol_policy.rb +98 -0
  171. data/lib/woods/mcp/provider_probe.rb +45 -6
  172. data/lib/woods/mcp/renderers/markdown_renderer.rb +72 -4
  173. data/lib/woods/mcp/renderers/plain_renderer.rb +54 -6
  174. data/lib/woods/mcp/server.rb +898 -152
  175. data/lib/woods/mcp/tasks/extension.rb +196 -0
  176. data/lib/woods/mcp/tasks/request_capture.rb +45 -0
  177. data/lib/woods/mcp/tasks/store.rb +518 -0
  178. data/lib/woods/mcp/tool_contract.rb +171 -0
  179. data/lib/woods/mcp/tool_response_renderer.rb +7 -0
  180. data/lib/woods/model_name_cache.rb +19 -1
  181. data/lib/woods/notion/client.rb +132 -36
  182. data/lib/woods/notion/exporter.rb +456 -61
  183. data/lib/woods/notion/mappers/column_mapper.rb +34 -5
  184. data/lib/woods/notion/mappers/migration_mapper.rb +32 -8
  185. data/lib/woods/notion/mappers/model_mapper.rb +21 -6
  186. data/lib/woods/notion/mappers/shared.rb +45 -3
  187. data/lib/woods/notion/sync_manifest.rb +258 -0
  188. data/lib/woods/obsidian/errors.rb +6 -0
  189. data/lib/woods/obsidian/name_mapper.rb +40 -24
  190. data/lib/woods/obsidian/vault_exporter.rb +103 -36
  191. data/lib/woods/operator/pipeline_guard.rb +118 -21
  192. data/lib/woods/operator/status_reporter.rb +20 -3
  193. data/lib/woods/path_dispatcher.rb +276 -0
  194. data/lib/woods/payload_store.rb +236 -0
  195. data/lib/woods/published_index/edge_shaper.rb +61 -0
  196. data/lib/woods/published_index/generation_catalog.rb +72 -0
  197. data/lib/woods/published_index/typed_unit_reader.rb +48 -0
  198. data/lib/woods/published_index.rb +287 -0
  199. data/lib/woods/railtie.rb +69 -30
  200. data/lib/woods/railtie_support.rb +167 -0
  201. data/lib/woods/release.rb +12 -0
  202. data/lib/woods/reload_policy.rb +206 -0
  203. data/lib/woods/resilience/circuit_breaker.rb +47 -8
  204. data/lib/woods/resilience/index_validator.rb +296 -10
  205. data/lib/woods/resilience/retryable_provider.rb +71 -6
  206. data/lib/woods/resolved_config.rb +55 -11
  207. data/lib/woods/retrieval/context_assembler.rb +132 -40
  208. data/lib/woods/retrieval/query_classifier.rb +26 -8
  209. data/lib/woods/retrieval/ranker.rb +193 -28
  210. data/lib/woods/retrieval/search_executor.rb +206 -39
  211. data/lib/woods/retriever.rb +317 -71
  212. data/lib/woods/retry_after.rb +22 -2
  213. data/lib/woods/ruby_analyzer/class_analyzer.rb +10 -14
  214. data/lib/woods/ruby_analyzer/fqn_builder.rb +2 -0
  215. data/lib/woods/ruby_analyzer/mermaid_renderer.rb +14 -4
  216. data/lib/woods/ruby_analyzer/method_analyzer.rb +1 -1
  217. data/lib/woods/ruby_analyzer/trace_enricher.rb +3 -0
  218. data/lib/woods/ruby_analyzer.rb +21 -5
  219. data/lib/woods/session_tracer/file_store.rb +138 -19
  220. data/lib/woods/session_tracer/middleware.rb +1 -2
  221. data/lib/woods/session_tracer/redis_store.rb +122 -12
  222. data/lib/woods/session_tracer/session_flow_assembler.rb +57 -17
  223. data/lib/woods/session_tracer/session_flow_document.rb +56 -14
  224. data/lib/woods/session_tracer/solid_cache_coordination.rb +192 -0
  225. data/lib/woods/session_tracer/solid_cache_store.rb +560 -91
  226. data/lib/woods/session_tracer/store.rb +14 -1
  227. data/lib/woods/storage/metadata_store.rb +230 -26
  228. data/lib/woods/storage/pgvector.rb +180 -22
  229. data/lib/woods/storage/qdrant.rb +367 -41
  230. data/lib/woods/storage/snapshotter/metadata.rb +79 -16
  231. data/lib/woods/storage/snapshotter/vector.rb +128 -17
  232. data/lib/woods/storage/snapshotter.rb +23 -5
  233. data/lib/woods/storage/vector_store.rb +49 -8
  234. data/lib/woods/storage_identity.rb +28 -0
  235. data/lib/woods/tasks.rb +53 -2
  236. data/lib/woods/temporal/json_snapshot_store.rb +112 -42
  237. data/lib/woods/temporal/snapshot_store.rb +139 -42
  238. data/lib/woods/unblocked/client.rb +119 -17
  239. data/lib/woods/unblocked/document_builder.rb +34 -2
  240. data/lib/woods/unblocked/exporter.rb +63 -27
  241. data/lib/woods/unblocked/rate_limiter.rb +23 -9
  242. data/lib/woods/unblocked/sync_manifest.rb +16 -8
  243. data/lib/woods/update_check.rb +24 -1
  244. data/lib/woods/util/uuid5.rb +124 -0
  245. data/lib/woods/version.rb +1 -1
  246. data/lib/woods/watch/daemon.rb +1345 -0
  247. data/lib/woods/watch/listen_watcher.rb +81 -0
  248. data/lib/woods/watch/polling_watcher.rb +137 -0
  249. data/lib/woods/watch/status.rb +169 -0
  250. data/lib/woods/watch/tree_scan.rb +163 -0
  251. data/lib/woods/watch/watcher.rb +100 -0
  252. data/lib/woods.rb +138 -9
  253. data/plugin/.claude-plugin/plugin.json +18 -0
  254. data/plugin/hooks/hooks.json +29 -0
  255. data/plugin/hooks/woods-post-edit.sh +226 -0
  256. data/plugin/hooks/woods-session-start.sh +77 -0
  257. data/plugin/skills/woods-agent-enable/SKILL.md +51 -0
  258. data/plugin/skills/woods-diagnose/SKILL.md +75 -0
  259. data/plugin/skills/woods-investigate/SKILL.md +39 -0
  260. data/plugin/skills/woods-mcp-config/SKILL.md +101 -0
  261. data/plugin/skills/woods-setup/SKILL.md +99 -0
  262. metadata +134 -23
  263. data/lib/woods/console/adapters/cache_adapter.rb +0 -58
  264. data/lib/woods/console/adapters/good_job_adapter.rb +0 -33
  265. data/lib/woods/console/adapters/job_adapter.rb +0 -74
  266. data/lib/woods/console/adapters/sidekiq_adapter.rb +0 -33
  267. data/lib/woods/console/adapters/solid_queue_adapter.rb +0 -33
  268. data/lib/woods/console/bridge.rb +0 -210
  269. data/lib/woods/formatting/claude_adapter.rb +0 -98
  270. data/lib/woods/formatting/generic_adapter.rb +0 -56
  271. data/lib/woods/formatting/gpt_adapter.rb +0 -64
  272. data/lib/woods/notion/mapper.rb +0 -40
  273. data/lib/woods/observability/health_check.rb +0 -79
  274. data/lib/woods/observability/instrumentation.rb +0 -34
@@ -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
- # Communicates with a bridge process running inside the Rails environment
28
- # via JSON-lines over stdio. Exposes 31 tools across 4 tiers
29
- # (9 read-only / 9 domain-aware / 10 analytics / 3 guarded) through MCP.
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.build(config: config)
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
- # - `console_credential_defense_enabled` is false
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 the caller has opted into the unsafe `console_eval`
77
- # scaffolding via `WOODS_CONSOLE_UNSAFE_EVAL=true` or an explicit
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
- # Enforce the `console_eval` opt-in safety contract at boot.
96
- #
97
- # When `WOODS_CONSOLE_UNSAFE_EVAL` is on:
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
- if defined?(Rails) && Rails.respond_to?(:env) && Rails.env.respond_to?(:production?) &&
110
- Rails.env.production?
111
- raise Woods::ConfigurationError,
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
- # Build a configured MCP::Server with console tools using the bridge protocol.
128
- #
129
- # Layer 1 limitation in bridge mode:
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
- # @return [MCP::Server] Configured server ready for transport
146
- def build(config:)
147
- connection_config = config['console'] || config
148
- conn_mgr = ConnectionManager.new(config: connection_config)
149
- redacted_columns = Array(config['redacted_columns'] || connection_config['redacted_columns'])
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] Approval callback for
172
- # `console_eval`. Required when the opt-in is on; the server refuses to
173
- # boot without it. Ignored when the opt-in is off.
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 = build_safe_context(redacted_columns, redacted_key_values)
187
- ctx = build_response_context(safe_ctx: safe_ctx, model_tables: model_tables,
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: 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
- build_server(executor, ctx)
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 three-collaborator eval wiring for the current config.
214
- #
215
- # When `unsafe_eval_enabled?` is false (default), returns nil for all
216
- # three collaborators the executor keeps its hard refusal and
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
- integer_keys: integer_property_keys(spec.properties),
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
- schema = { properties: spec.properties }
359
- schema[:required] = spec.required if spec.required&.any?
360
- schema
207
+ spec.input_schema
361
208
  end
362
209
 
363
- # Pre-compute property keys declared as integer in a schema.
364
- #
365
- # @param properties [Hash] Tool schema properties
366
- # @return [Array<Symbol>]
367
- def integer_property_keys(properties)
368
- properties.select { |_k, v| v[:type] == 'integer' }.keys.map(&:to_sym)
369
- end
370
-
371
- # Build a SafeContext (Layer 3) from redaction settings, or nil when nothing is configured.
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 build_safe_context(redacted_columns, redacted_key_values)
377
- return nil unless redacted_columns.any? || redacted_key_values.any?
378
-
379
- SafeContext.new(
380
- connection: nil,
381
- redacted_columns: redacted_columns,
382
- redacted_key_values: redacted_key_values
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 [ConnectionManager, EmbeddedExecutor] Any object with send_request(Hash) -> Hash
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
- register_tier1_tools(server, conn_mgr, ctx, renderer: renderer)
481
- register_tier2_tools(server, conn_mgr, ctx, renderer: renderer)
482
- register_tier3_tools(server, conn_mgr, ctx, renderer: renderer)
483
- register_tier4_tools(server, conn_mgr, ctx, renderer: renderer)
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