woods 1.6.4 → 2.0.0.beta1

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 (282) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +1879 -37
  3. data/CONTRIBUTING.md +195 -137
  4. data/README.md +162 -520
  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 +620 -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 +415 -0
  19. data/docs/INTERNALS.md +415 -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 +197 -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 +39 -3
  36. data/exe/woods-console-mcp +21 -35
  37. data/exe/woods-mcp +20 -7
  38. data/exe/woods-mcp-http +78 -24
  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 +40 -1
  52. data/lib/woods/builder.rb +310 -22
  53. data/lib/woods/cache/cache_middleware.rb +18 -13
  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/credential_index.rb +5 -53
  64. data/lib/woods/console/credential_scanner.rb +15 -16
  65. data/lib/woods/console/dispatch_pipeline.rb +46 -34
  66. data/lib/woods/console/embedded_executor.rb +806 -257
  67. data/lib/woods/console/eval_guard.rb +27 -20
  68. data/lib/woods/console/input_contract.rb +78 -0
  69. data/lib/woods/console/model_validator.rb +24 -6
  70. data/lib/woods/console/rack_middleware.rb +62 -63
  71. data/lib/woods/console/redactor.rb +10 -24
  72. data/lib/woods/console/safe_context.rb +45 -45
  73. data/lib/woods/console/scope_predicate_parser.rb +41 -0
  74. data/lib/woods/console/server.rb +136 -267
  75. data/lib/woods/console/sql_noise_stripper.rb +20 -51
  76. data/lib/woods/console/sql_table_scanner.rb +39 -90
  77. data/lib/woods/console/sql_validator.rb +455 -85
  78. data/lib/woods/console/table_gate.rb +2 -2
  79. data/lib/woods/console/tool_specs.rb +462 -88
  80. data/lib/woods/console/tools/tier1.rb +0 -3
  81. data/lib/woods/console/tools/tier4.rb +17 -7
  82. data/lib/woods/coordination/lock_heartbeat.rb +103 -0
  83. data/lib/woods/coordination/pipeline_lock.rb +263 -53
  84. data/lib/woods/db/migrations/007_typed_snapshot_units.rb +45 -0
  85. data/lib/woods/db/migrator.rb +3 -9
  86. data/lib/woods/db/schema_version.rb +47 -2
  87. data/lib/woods/dependency_graph.rb +898 -64
  88. data/lib/woods/embedding/fake.rb +138 -0
  89. data/lib/woods/embedding/indexer.rb +832 -40
  90. data/lib/woods/embedding/openai.rb +77 -19
  91. data/lib/woods/embedding/provider.rb +189 -11
  92. data/lib/woods/embedding/text_preparer.rb +1 -1
  93. data/lib/woods/embedding/token_counter.rb +0 -7
  94. data/lib/woods/evaluation/ablation_agent_payload.rb +38 -0
  95. data/lib/woods/evaluation/ablation_executor.rb +67 -0
  96. data/lib/woods/evaluation/ablation_provenance.rb +38 -0
  97. data/lib/woods/evaluation/ablation_report_writer.rb +43 -0
  98. data/lib/woods/evaluation/ablation_runner.rb +173 -0
  99. data/lib/woods/evaluation/ablation_summary.rb +65 -0
  100. data/lib/woods/evaluation/ablation_task.rb +66 -0
  101. data/lib/woods/evaluation/ablation_task_set.rb +77 -0
  102. data/lib/woods/evaluation/ablation_timed_executor.rb +91 -0
  103. data/lib/woods/evaluation/ablation_worktree.rb +71 -0
  104. data/lib/woods/evaluation/baseline.rb +60 -0
  105. data/lib/woods/evaluation/baseline_runner.rb +11 -3
  106. data/lib/woods/evaluation/evaluator.rb +41 -8
  107. data/lib/woods/evaluation/query_set.rb +79 -13
  108. data/lib/woods/evaluation/report_generator.rb +20 -1
  109. data/lib/woods/export/unit_facts.rb +0 -11
  110. data/lib/woods/extracted_unit.rb +22 -63
  111. data/lib/woods/extractor.rb +2503 -192
  112. data/lib/woods/extractors/action_cable_extractor.rb +9 -4
  113. data/lib/woods/extractors/ast_source_extraction.rb +20 -2
  114. data/lib/woods/extractors/caching_extractor.rb +46 -12
  115. data/lib/woods/extractors/callback_analyzer.rb +39 -9
  116. data/lib/woods/extractors/component_discovery.rb +123 -0
  117. data/lib/woods/extractors/concern_extractor.rb +17 -3
  118. data/lib/woods/extractors/controller_extractor.rb +389 -29
  119. data/lib/woods/extractors/decorator_extractor.rb +7 -14
  120. data/lib/woods/extractors/engine_extractor.rb +53 -8
  121. data/lib/woods/extractors/event_extractor.rb +55 -4
  122. data/lib/woods/extractors/factory_extractor.rb +49 -11
  123. data/lib/woods/extractors/graphql_extractor.rb +162 -66
  124. data/lib/woods/extractors/i18n_extractor.rb +6 -1
  125. data/lib/woods/extractors/job_extractor.rb +51 -21
  126. data/lib/woods/extractors/lib_extractor.rb +23 -17
  127. data/lib/woods/extractors/line_neutralizer.rb +171 -0
  128. data/lib/woods/extractors/mailer_extractor.rb +9 -1
  129. data/lib/woods/extractors/manager_extractor.rb +19 -2
  130. data/lib/woods/extractors/migration_extractor.rb +22 -11
  131. data/lib/woods/extractors/model_extractor.rb +292 -57
  132. data/lib/woods/extractors/package_extractor.rb +154 -0
  133. data/lib/woods/extractors/phlex_extractor.rb +18 -3
  134. data/lib/woods/extractors/policy_extractor.rb +6 -5
  135. data/lib/woods/extractors/poro_extractor.rb +13 -14
  136. data/lib/woods/extractors/pundit_extractor.rb +3 -3
  137. data/lib/woods/extractors/rails_source_extractor.rb +24 -7
  138. data/lib/woods/extractors/rake_task_extractor.rb +158 -30
  139. data/lib/woods/extractors/reference_patterns.rb +38 -0
  140. data/lib/woods/extractors/route_extractor.rb +58 -2
  141. data/lib/woods/extractors/scheduled_job_extractor.rb +51 -35
  142. data/lib/woods/extractors/serializer_extractor.rb +3 -4
  143. data/lib/woods/extractors/service_extractor.rb +11 -1
  144. data/lib/woods/extractors/shared_dependency_scanner.rb +24 -34
  145. data/lib/woods/extractors/shared_utility_methods.rb +36 -6
  146. data/lib/woods/extractors/source_nesting.rb +560 -0
  147. data/lib/woods/extractors/state_machine_extractor.rb +30 -18
  148. data/lib/woods/extractors/test_mapping_extractor.rb +26 -9
  149. data/lib/woods/extractors/view_component_extractor.rb +28 -3
  150. data/lib/woods/extractors/view_engines/erb.rb +17 -3
  151. data/lib/woods/feedback/gap_detector.rb +9 -3
  152. data/lib/woods/feedback/store.rb +7 -1
  153. data/lib/woods/filename_utils.rb +29 -1
  154. data/lib/woods/flow_analysis/operation_extractor.rb +22 -10
  155. data/lib/woods/flow_assembler.rb +63 -21
  156. data/lib/woods/flow_document.rb +1 -0
  157. data/lib/woods/flow_precomputer.rb +138 -22
  158. data/lib/woods/gem_mapper.rb +285 -0
  159. data/lib/woods/generation.rb +185 -0
  160. data/lib/woods/git_command.rb +38 -0
  161. data/lib/woods/git_provenance.rb +16 -2
  162. data/lib/woods/graph_analyzer.rb +408 -34
  163. data/lib/woods/index_artifact.rb +93 -23
  164. data/lib/woods/mcp/bearer_auth.rb +92 -22
  165. data/lib/woods/mcp/bootstrap_state.rb +77 -0
  166. data/lib/woods/mcp/bootstrapper.rb +582 -77
  167. data/lib/woods/mcp/config_resolver.rb +66 -6
  168. data/lib/woods/mcp/errors.rb +60 -0
  169. data/lib/woods/mcp/index_reader.rb +836 -117
  170. data/lib/woods/mcp/index_reader_pinning.rb +78 -0
  171. data/lib/woods/mcp/origin_guard.rb +108 -23
  172. data/lib/woods/mcp/protocol_policy.rb +98 -0
  173. data/lib/woods/mcp/provider_probe.rb +45 -6
  174. data/lib/woods/mcp/renderers/markdown_renderer.rb +72 -4
  175. data/lib/woods/mcp/renderers/plain_renderer.rb +54 -6
  176. data/lib/woods/mcp/server.rb +907 -154
  177. data/lib/woods/mcp/tasks/extension.rb +196 -0
  178. data/lib/woods/mcp/tasks/request_capture.rb +45 -0
  179. data/lib/woods/mcp/tasks/store.rb +518 -0
  180. data/lib/woods/mcp/tool_contract.rb +171 -0
  181. data/lib/woods/mcp/tool_response_renderer.rb +7 -0
  182. data/lib/woods/mcp/version_aware_tool_dispatch.rb +3 -9
  183. data/lib/woods/model_name_cache.rb +19 -1
  184. data/lib/woods/notion/client.rb +132 -36
  185. data/lib/woods/notion/exporter.rb +456 -61
  186. data/lib/woods/notion/mappers/column_mapper.rb +34 -5
  187. data/lib/woods/notion/mappers/migration_mapper.rb +32 -8
  188. data/lib/woods/notion/mappers/model_mapper.rb +21 -6
  189. data/lib/woods/notion/mappers/shared.rb +45 -3
  190. data/lib/woods/notion/sync_manifest.rb +258 -0
  191. data/lib/woods/obsidian/errors.rb +6 -0
  192. data/lib/woods/obsidian/name_mapper.rb +40 -24
  193. data/lib/woods/obsidian/vault_exporter.rb +103 -36
  194. data/lib/woods/operator/pipeline_guard.rb +118 -21
  195. data/lib/woods/operator/status_reporter.rb +20 -3
  196. data/lib/woods/path_dispatcher.rb +276 -0
  197. data/lib/woods/payload_store.rb +223 -0
  198. data/lib/woods/published_index/edge_shaper.rb +61 -0
  199. data/lib/woods/published_index/generation_catalog.rb +72 -0
  200. data/lib/woods/published_index/typed_unit_reader.rb +48 -0
  201. data/lib/woods/published_index.rb +287 -0
  202. data/lib/woods/railtie.rb +70 -38
  203. data/lib/woods/railtie_support.rb +167 -0
  204. data/lib/woods/release.rb +12 -0
  205. data/lib/woods/reload_policy.rb +206 -0
  206. data/lib/woods/resilience/circuit_breaker.rb +47 -8
  207. data/lib/woods/resilience/index_validator.rb +296 -10
  208. data/lib/woods/resilience/retryable_provider.rb +71 -6
  209. data/lib/woods/resolved_config.rb +55 -11
  210. data/lib/woods/retrieval/context_assembler.rb +132 -40
  211. data/lib/woods/retrieval/query_classifier.rb +25 -6
  212. data/lib/woods/retrieval/ranker.rb +193 -28
  213. data/lib/woods/retrieval/search_executor.rb +206 -39
  214. data/lib/woods/retriever.rb +317 -71
  215. data/lib/woods/retry_after.rb +22 -2
  216. data/lib/woods/ruby_analyzer/class_analyzer.rb +10 -14
  217. data/lib/woods/ruby_analyzer/fqn_builder.rb +2 -0
  218. data/lib/woods/ruby_analyzer/mermaid_renderer.rb +14 -4
  219. data/lib/woods/ruby_analyzer/method_analyzer.rb +1 -1
  220. data/lib/woods/ruby_analyzer.rb +21 -5
  221. data/lib/woods/session_tracer/file_store.rb +138 -19
  222. data/lib/woods/session_tracer/redis_store.rb +122 -12
  223. data/lib/woods/session_tracer/session_flow_assembler.rb +54 -11
  224. data/lib/woods/session_tracer/session_flow_document.rb +52 -6
  225. data/lib/woods/session_tracer/solid_cache_coordination.rb +192 -0
  226. data/lib/woods/session_tracer/solid_cache_store.rb +560 -91
  227. data/lib/woods/session_tracer/store.rb +14 -1
  228. data/lib/woods/storage/metadata_store.rb +230 -26
  229. data/lib/woods/storage/pgvector.rb +180 -22
  230. data/lib/woods/storage/qdrant.rb +367 -41
  231. data/lib/woods/storage/snapshotter/metadata.rb +79 -16
  232. data/lib/woods/storage/snapshotter/vector.rb +128 -17
  233. data/lib/woods/storage/snapshotter.rb +23 -5
  234. data/lib/woods/storage/vector_store.rb +49 -8
  235. data/lib/woods/storage_identity.rb +28 -0
  236. data/lib/woods/tasks.rb +53 -2
  237. data/lib/woods/temporal/json_snapshot_store.rb +112 -42
  238. data/lib/woods/temporal/snapshot_store.rb +139 -42
  239. data/lib/woods/unblocked/client.rb +119 -17
  240. data/lib/woods/unblocked/document_builder.rb +34 -2
  241. data/lib/woods/unblocked/exporter.rb +63 -27
  242. data/lib/woods/unblocked/rate_limiter.rb +23 -9
  243. data/lib/woods/unblocked/sync_manifest.rb +16 -8
  244. data/lib/woods/update_check.rb +24 -1
  245. data/lib/woods/util/uuid5.rb +124 -0
  246. data/lib/woods/version.rb +1 -1
  247. data/lib/woods/watch/daemon.rb +1345 -0
  248. data/lib/woods/watch/listen_watcher.rb +81 -0
  249. data/lib/woods/watch/polling_watcher.rb +137 -0
  250. data/lib/woods/watch/status.rb +169 -0
  251. data/lib/woods/watch/tree_scan.rb +163 -0
  252. data/lib/woods/watch/watcher.rb +100 -0
  253. data/lib/woods.rb +53 -9
  254. data/plugin/.claude-plugin/plugin.json +18 -0
  255. data/plugin/hooks/hooks.json +29 -0
  256. data/plugin/hooks/woods-post-edit.sh +226 -0
  257. data/plugin/hooks/woods-session-start.sh +77 -0
  258. data/plugin/skills/woods-agent-enable/SKILL.md +51 -0
  259. data/plugin/skills/woods-diagnose/SKILL.md +75 -0
  260. data/plugin/skills/woods-investigate/SKILL.md +39 -0
  261. data/plugin/skills/woods-mcp-config/SKILL.md +101 -0
  262. data/plugin/skills/woods-setup/SKILL.md +99 -0
  263. metadata +102 -30
  264. data/lib/woods/console/adapter_family.rb +0 -39
  265. data/lib/woods/console/adapters/cache_adapter.rb +0 -58
  266. data/lib/woods/console/adapters/good_job_adapter.rb +0 -33
  267. data/lib/woods/console/adapters/job_adapter.rb +0 -74
  268. data/lib/woods/console/adapters/sidekiq_adapter.rb +0 -33
  269. data/lib/woods/console/adapters/solid_queue_adapter.rb +0 -33
  270. data/lib/woods/console/bridge.rb +0 -210
  271. data/lib/woods/console/credential_scanner_registry.rb +0 -36
  272. data/lib/woods/console/encrypted_credential_snapshot.rb +0 -16
  273. data/lib/woods/console/sql_output_policy.rb +0 -535
  274. data/lib/woods/console/sqlite_read_guard.rb +0 -46
  275. data/lib/woods/formatting/claude_adapter.rb +0 -98
  276. data/lib/woods/formatting/generic_adapter.rb +0 -56
  277. data/lib/woods/formatting/gpt_adapter.rb +0 -64
  278. data/lib/woods/mcp/http_transport_options.rb +0 -15
  279. data/lib/woods/mcp/origin_policy.rb +0 -113
  280. data/lib/woods/notion/mapper.rb +0 -40
  281. data/lib/woods/observability/health_check.rb +0 -79
  282. data/lib/woods/observability/instrumentation.rb +0 -34
@@ -39,6 +39,15 @@ module Woods
39
39
  # Suffix pattern — longest suffix match wins because we scan the full list.
40
40
  SUFFIX_PATTERN = /(_eq|_not_eq|_gteq|_lteq|_gt|_lt|_not_in|_not_null|_in|_null|_present|_blank|_matches)\z/
41
41
 
42
+ # Suffix classes, each with its own required value type. Ruby truthiness
43
+ # treats any non-nil, non-false value as true — so without a strict type
44
+ # check, `{status_present: "false"}` (a JSON string) inverts the caller's
45
+ # intent instead of raising. `_eq`/`_not_eq`/`_matches` accept any scalar
46
+ # and are intentionally not classified here.
47
+ EXISTENCE_SUFFIXES = %w[_null _not_null _present _blank].freeze
48
+ COMPARISON_SUFFIXES = %w[_gt _gteq _lt _lteq].freeze
49
+ SET_SUFFIXES = %w[_in _not_in].freeze
50
+
42
51
  SUFFIX_HINT = "Supported suffixes: #{SUPPORTED_SUFFIXES.join(', ')}.".freeze
43
52
 
44
53
  # @param model_name [String] ActiveRecord model name (e.g. 'Order')
@@ -70,8 +79,10 @@ module Woods
70
79
  suffix = match[1]
71
80
  column = key.delete_suffix(suffix)
72
81
  @model_validator.validate_column!(@model_name, column)
82
+ validate_suffix_value_type!(suffix, value)
73
83
  arel_nodes << build_node(relation, column, suffix, value)
74
84
  else
85
+ @model_validator.validate_column!(@model_name, key)
75
86
  equality[raw_key] = value
76
87
  end
77
88
  end
@@ -83,6 +94,36 @@ module Woods
83
94
 
84
95
  private
85
96
 
97
+ # Enforce the suffix-dependent value type before any Arel node is
98
+ # built. `true`/`false` are checked by identity (`==`), not truthiness,
99
+ # so a JSON string like `"false"` is rejected rather than silently
100
+ # treated as truthy.
101
+ #
102
+ # @param suffix [String] One of SUPPORTED_SUFFIXES
103
+ # @param value [Object] The predicate value
104
+ # @raise [ValidationError] when the value's type doesn't match the suffix class
105
+ def validate_suffix_value_type!(suffix, value)
106
+ case suffix
107
+ when *EXISTENCE_SUFFIXES
108
+ return if [true, false].include?(value)
109
+
110
+ raise ValidationError,
111
+ "Predicate suffix '#{suffix}' requires a strict boolean value " \
112
+ "(got #{value.class}: #{value.inspect}). #{SUFFIX_HINT}"
113
+ when *COMPARISON_SUFFIXES
114
+ return if value.is_a?(String) || value.is_a?(Numeric)
115
+
116
+ raise ValidationError,
117
+ "Predicate suffix '#{suffix}' requires a scalar (String or Numeric) value " \
118
+ "(got #{value.class}: #{value.inspect}). #{SUFFIX_HINT}"
119
+ end
120
+ # _in/_not_in are intentionally not type-checked here: build_node
121
+ # already wraps a scalar in a single-element Array via `Array(value)`,
122
+ # a pre-existing, separately-tested convenience for callers building
123
+ # scope hashes programmatically. The public JSON Schema still
124
+ # requires a real array for _in/_not_in at the MCP boundary.
125
+ end
126
+
86
127
  # Build an Arel predicate node for a validated column + suffix.
87
128
  #
88
129
  # @param relation [ActiveRecord::Relation, Class] Used to get the arel_table
@@ -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'
@@ -13,7 +14,6 @@ require_relative 'audit_logger'
13
14
  require_relative 'confirmation'
14
15
  require_relative 'console_response_renderer'
15
16
  require_relative 'credential_scanner'
16
- require_relative 'credential_scanner_registry'
17
17
  require_relative 'eval_guard'
18
18
  require_relative 'table_gate'
19
19
  require_relative 'redactor'
@@ -22,38 +22,38 @@ require_relative 'dispatch_pipeline'
22
22
  require_relative 'tool_specs'
23
23
 
24
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
+
25
31
  module Console
26
32
  # Console MCP Server — queries live Rails application state.
27
33
  #
28
- # Communicates with a bridge process running inside the Rails environment
29
- # via JSON-lines over stdio. Exposes 31 tools across 4 tiers
30
- # (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.
31
37
  #
32
38
  # @example
33
- # server = Woods::Console::Server.build(config: config)
39
+ # server = Woods::Console::Server.build_embedded(
40
+ # model_validator: validator,
41
+ # safe_context: safe_context
42
+ # )
34
43
  # transport = MCP::Server::Transports::StdioTransport.new(server)
35
44
  # transport.open
36
45
  #
37
46
  module Server # rubocop:disable Metrics/ModuleLength
38
- SCANNERS = CredentialScannerRegistry.new
39
- private_constant :SCANNERS
40
-
41
47
  class << self # rubocop:disable Metrics/ClassLength
42
48
  # Rebuild the boot-time credential index from fresh Rails credentials
43
- # and hot-swap it into every live scanner without restarting the process.
49
+ # and hot-swap it into the active scanner without restarting the process.
44
50
  #
45
- # Refresh failures propagate without replacing any live index.
46
51
  # Host rotation jobs should call this immediately after `rails credentials:edit`
47
52
  # changes are deployed. The swap is atomic on MRI (GVL) — in-flight scans see
48
53
  # either the old or the new index, never a partial one.
49
54
  #
50
- # Returns nil when:
51
- # - `console_credential_defense_enabled` is false
52
- # - No server has been built yet in this process (`build` / `build_embedded`
53
- # have not been called)
54
- #
55
- # Existing callers of `build` / `build_embedded` are unaffected — this is an
56
- # 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.
57
57
  #
58
58
  # @param rails_app [#credentials] The Rails application to re-read.
59
59
  # Defaults to `Rails.application` when `Rails` is defined, otherwise
@@ -62,11 +62,14 @@ module Woods
62
62
  # the rebuild was skipped.
63
63
  def rebuild_credential_index(rails_app: nil)
64
64
  return nil unless credential_defense_enabled?
65
+ return nil unless @active_scanner
65
66
 
66
67
  target_app = rails_app || default_rails_app
67
68
  return nil unless target_app
68
69
 
69
- SCANNERS.rebuild { CredentialIndex.refresh(rails_app: target_app) }
70
+ new_index = CredentialIndex.build(rails_app: target_app)
71
+ @active_scanner.replace_index!(new_index)
72
+ new_index
70
73
  end
71
74
 
72
75
  # True when Woods is configured and credential defense is on.
@@ -75,15 +78,8 @@ module Woods
75
78
  config&.console_credential_defense_enabled ? true : false
76
79
  end
77
80
 
78
- # True when the caller has opted into the unsafe `console_eval`
79
- # scaffolding via `WOODS_CONSOLE_UNSAFE_EVAL=true` or an explicit
80
- # `config.console_unsafe_eval_enabled = true`. Explicit config wins
81
- # over the env var in both directions.
82
- #
83
- # NOTE: returning true here does NOT enable eval execution. The
84
- # execution path is deliberately unimplemented (backlog
85
- # unsafe-eval-opt-in). This predicate only governs the boot-time
86
- # 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.
87
83
  #
88
84
  # @return [Boolean]
89
85
  def unsafe_eval_enabled?
@@ -94,29 +90,13 @@ module Woods
94
90
  ENV['WOODS_CONSOLE_UNSAFE_EVAL'] == 'true'
95
91
  end
96
92
 
97
- # Enforce the `console_eval` opt-in safety contract at boot.
98
- #
99
- # When `WOODS_CONSOLE_UNSAFE_EVAL` is on:
100
- # - refuse outright in `Rails.env.production?` (non-negotiable),
101
- # - otherwise emit a LOUD stderr banner so operators know the flag
102
- # is live even though eval remains unimplemented.
103
- #
104
- # Safe when Rails is not loaded (specs, non-Rails hosts).
105
- #
106
- # @return [void]
107
- # @raise [Woods::ConfigurationError] when the flag is on in production.
108
- def enforce_unsafe_eval_contract!
109
- return unless unsafe_eval_enabled?
110
-
111
- if defined?(Rails) && Rails.respond_to?(:env) && Rails.env.respond_to?(:production?) &&
112
- Rails.env.production?
113
- raise Woods::ConfigurationError,
114
- 'WOODS_CONSOLE_UNSAFE_EVAL is set but Rails.env.production? is true. ' \
115
- 'console_eval cannot be opted into in production. Unset the flag or ' \
116
- 'restart in a non-production environment.'
117
- end
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
118
96
 
119
- 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.'
120
100
  end
121
101
 
122
102
  # Resolves `Rails.application` when available, else nil.
@@ -126,36 +106,15 @@ module Woods
126
106
  Rails.application
127
107
  end
128
108
 
129
- # Build a configured MCP::Server with console tools using the bridge protocol.
130
- #
131
- # ⚠ Layer 1 limitation in bridge mode:
132
- # The server side of the bridge has no access to the remote app's
133
- # `ActiveRecord::Base.descendants`, so model_tables and model_reflections
134
- # are empty. `TableGate#check_sql!` still fires against the raw SQL
135
- # argument of `console_sql`, but `check_model!`, `check_joins!`, and
136
- # `check_association!` are effectively no-ops for tools that receive a
137
- # model name rather than SQL (find, sample, count, etc.). A bridge-mode
138
- # deployment therefore relies on Layer 2 (credential scanning) + Layer 3
139
- # (column/EAV redaction) + Layer 4 (SqlValidator + SafeContext rollback)
140
- # for non-SQL tool calls. If you need full Layer 1 coverage, use
141
- # `build_embedded` (the stdio entry point `exe/woods-console` does this).
142
- #
143
- # See docs/CONSOLE_MCP_SETUP.md "Bridge vs. embedded defense coverage"
144
- # for the full matrix.
145
- #
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.
146
112
  # @param config [Hash] Configuration hash (from YAML or env)
147
- # @return [MCP::Server] Configured server ready for transport
148
- def build(config:)
149
- connection_config = config['console'] || config
150
- conn_mgr = ConnectionManager.new(config: connection_config)
151
- redacted_columns = Array(config['redacted_columns'] || connection_config['redacted_columns'])
152
- redacted_key_values = Array(
153
- config['redacted_key_values'] || connection_config['redacted_key_values']
154
- )
155
- safe_ctx = build_safe_context(redacted_columns, redacted_key_values)
156
- ctx = build_response_context(safe_ctx: safe_ctx, model_tables: {}, model_reflections: {})
157
-
158
- 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.'
159
118
  end
160
119
 
161
120
  # Build a configured MCP::Server using embedded ActiveRecord execution.
@@ -170,156 +129,43 @@ module Woods
170
129
  # {key_column:, value_column:, sensitive_keys: []}. See SafeContext for semantics.
171
130
  # @param connection [Object, nil] Database connection for adapter detection
172
131
  # @param read_tools_enabled [Boolean] Enable sql/query tools in embedded mode (default: false)
173
- # @param unsafe_eval_confirmation [Confirmation, nil] Approval callback for
174
- # `console_eval`. Required when the opt-in is on; the server refuses to
175
- # boot without it. Ignored when the opt-in is off.
176
- # @param unsafe_eval_audit_log_path [String, Pathname, nil] JSONL audit log
177
- # 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.
178
135
  # @return [MCP::Server] Configured server ready for transport
179
- def build_embedded(model_validator:, safe_context:, redacted_columns: [], # rubocop:disable Metrics/ParameterLists, Metrics/MethodLength
136
+ def build_embedded(model_validator:, safe_context:, redacted_columns: [], # rubocop:disable Metrics/ParameterLists
180
137
  redacted_key_values: [], connection: nil,
181
138
  read_tools_enabled: false, model_tables: {},
182
139
  model_reflections: {},
183
140
  unsafe_eval_confirmation: nil,
184
141
  unsafe_eval_audit_log_path: nil)
185
142
  require_relative 'embedded_executor'
186
- enforce_unsafe_eval_contract!
143
+ enforce_unsafe_eval_contract!(
144
+ legacy_options_present: unsafe_eval_confirmation || unsafe_eval_audit_log_path
145
+ )
187
146
 
188
- safe_ctx = build_safe_context(redacted_columns, redacted_key_values)
189
- 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,
190
149
  model_reflections: model_reflections)
191
150
 
192
- eval_wiring = build_unsafe_eval_wiring(
193
- confirmation: unsafe_eval_confirmation,
194
- audit_log_path: unsafe_eval_audit_log_path
195
- )
196
-
197
151
  # Wire the same TableGate into the executor so sql/query are blocked
198
152
  # PRE-execution against console_blocked_tables (previously TableGate
199
153
  # was only consulted on the render path, leaving the defense inert
200
154
  # for the sql and query tools).
201
- table_gate = ctx&.table_gate
202
155
  executor = EmbeddedExecutor.new(
203
- model_validator: model_validator, safe_context: safe_context,
204
- redaction_context: safe_ctx,
156
+ model_validator: model_validator, safe_context: safe_ctx,
205
157
  connection: connection, read_tools_enabled: read_tools_enabled,
206
- table_gate: table_gate,
207
- eval_guard: eval_wiring[:eval_guard],
208
- confirmation: eval_wiring[:confirmation],
209
- audit_logger: eval_wiring[:audit_logger],
210
- unsafe_eval_enabled: eval_wiring[:unsafe_eval_enabled]
158
+ table_gate: ctx&.table_gate
211
159
  )
212
160
 
213
- build_server(executor, ctx)
214
- end
215
-
216
- # Resolve the three-collaborator eval wiring for the current config.
217
- #
218
- # When `unsafe_eval_enabled?` is false (default), returns nil for all
219
- # three collaborators — the executor keeps its hard refusal and
220
- # EvalGuard is never reached.
221
- #
222
- # When it's true, BOTH a {Confirmation} and an audit-log path MUST be
223
- # provided (via kwargs or config); otherwise we raise so a
224
- # misconfigured host fails at boot instead of silently running Ruby
225
- # without approval or audit. See backlog B-053.
226
- #
227
- # @param confirmation [Confirmation, nil] Explicit kwarg wins over
228
- # `config.console_unsafe_eval_confirmation`.
229
- # @param audit_log_path [String, Pathname, nil] Explicit kwarg wins
230
- # over `config.console_unsafe_eval_audit_log_path`.
231
- # @return [Hash] { eval_guard:, confirmation:, audit_logger:, unsafe_eval_enabled: }
232
- # @raise [Woods::ConfigurationError] when opt-in is on but either
233
- # collaborator is missing.
234
- def build_unsafe_eval_wiring(confirmation:, audit_log_path:)
235
- return empty_unsafe_eval_wiring unless unsafe_eval_enabled?
236
-
237
- config = Woods.configuration if Woods.respond_to?(:configuration)
238
- confirmation ||= config&.console_unsafe_eval_confirmation
239
- audit_log_path ||= config&.console_unsafe_eval_audit_log_path
240
- require_unsafe_eval_collaborators!(confirmation, audit_log_path)
241
-
242
- {
243
- eval_guard: EvalGuard.new,
244
- confirmation: confirmation,
245
- audit_logger: AuditLogger.new(path: audit_log_path.to_s),
246
- unsafe_eval_enabled: true
247
- }
248
- end
249
-
250
- def empty_unsafe_eval_wiring
251
- { eval_guard: nil, confirmation: nil, audit_logger: nil, unsafe_eval_enabled: false }
252
- end
253
-
254
- def require_unsafe_eval_collaborators!(confirmation, audit_log_path)
255
- if confirmation.nil?
256
- raise Woods::ConfigurationError,
257
- 'WOODS_CONSOLE_UNSAFE_EVAL is set but no Confirmation was provided. ' \
258
- 'Pass `unsafe_eval_confirmation:` to Server.build_embedded / RackMiddleware ' \
259
- 'or set `config.console_unsafe_eval_confirmation`. Fail-closed by design — ' \
260
- 'see backlog B-053 / docs/CONSOLE_MCP_SETUP.md.'
261
- end
262
- return unless audit_log_path.nil? || audit_log_path.to_s.strip.empty?
263
-
264
- raise Woods::ConfigurationError,
265
- 'WOODS_CONSOLE_UNSAFE_EVAL is set but no audit-log path was provided. ' \
266
- 'Pass `unsafe_eval_audit_log_path:` to Server.build_embedded / RackMiddleware ' \
267
- 'or set `config.console_unsafe_eval_audit_log_path`. Every console_eval run ' \
268
- 'must be audited.'
269
- end
270
-
271
- # Register all tool specs for a given tier on the server.
272
- #
273
- # @param server [MCP::Server] The MCP server instance
274
- # @param conn_mgr [ConnectionManager, EmbeddedExecutor] Request executor
275
- # @param ctx [ResponseContext, nil] Optional context bundling response-safety layers
276
- # @param tier [Integer] Tier number (1-4)
277
- # @param renderer [ConsoleResponseRenderer, nil] Optional response renderer
278
- # @return [void]
279
- def register_tier_tools(server, conn_mgr, ctx, tier:, renderer: nil)
280
- TOOL_SPECS.select { |spec| spec.tier == tier }.each do |spec|
281
- register(spec, server, conn_mgr, ctx, renderer: renderer)
282
- end
283
- end
284
-
285
- # Register Tier 1 read-only tools on the server.
286
- #
287
- # @param server [MCP::Server] The MCP server instance
288
- # @param conn_mgr [ConnectionManager, EmbeddedExecutor] Request executor
289
- # @param ctx [ResponseContext, nil] Optional context for column redaction
290
- # @return [void]
291
- def register_tier1_tools(server, conn_mgr, ctx = nil, renderer: nil)
292
- register_tier_tools(server, conn_mgr, ctx, tier: 1, renderer: renderer)
161
+ mode = read_tools_enabled ? :embedded_read : :embedded
162
+ build_server(executor, ctx, tool_names: executable_tool_names(mode))
293
163
  end
294
164
 
295
- # Register Tier 2 domain-aware tools on the server.
296
- #
297
- # @param server [MCP::Server] The MCP server instance
298
- # @param conn_mgr [ConnectionManager, EmbeddedExecutor] Request executor
299
- # @param ctx [ResponseContext, nil] Optional context for column redaction
300
- # @return [void]
301
- def register_tier2_tools(server, conn_mgr, ctx = nil, renderer: nil)
302
- register_tier_tools(server, conn_mgr, ctx, tier: 2, renderer: renderer)
303
- end
304
-
305
- # Register Tier 3 analytics tools on the server.
306
- #
307
- # @param server [MCP::Server] The MCP server instance
308
- # @param conn_mgr [ConnectionManager, EmbeddedExecutor] Request executor
309
- # @param ctx [ResponseContext, nil] Optional context for column redaction
310
- # @return [void]
311
- def register_tier3_tools(server, conn_mgr, ctx = nil, renderer: nil)
312
- register_tier_tools(server, conn_mgr, ctx, tier: 3, renderer: renderer)
313
- end
314
-
315
- # Register Tier 4 guarded tools on the server.
316
- #
317
- # @param server [MCP::Server] The MCP server instance
318
- # @param conn_mgr [ConnectionManager, EmbeddedExecutor] Request executor
319
- # @param ctx [ResponseContext, nil] Optional context for column redaction
320
- # @return [void]
321
- def register_tier4_tools(server, conn_mgr, ctx = nil, renderer: nil)
322
- 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) }
323
169
  end
324
170
 
325
171
  private
@@ -340,7 +186,7 @@ module Woods
340
186
  pipeline = DispatchPipeline.new(
341
187
  tool_name: spec.name,
342
188
  handler: spec.handler,
343
- integer_keys: integer_property_keys(spec.properties),
189
+ properties: spec.properties,
344
190
  conn_mgr: conn_mgr,
345
191
  ctx: ctx || NullResponseContext.instance,
346
192
  renderer: renderer,
@@ -358,32 +204,72 @@ module Woods
358
204
  # @param spec [ToolSpec]
359
205
  # @return [Hash]
360
206
  def spec_schema(spec)
361
- schema = { properties: spec.properties }
362
- schema[:required] = spec.required if spec.required&.any?
363
- schema
364
- end
365
-
366
- # Pre-compute property keys declared as integer in a schema.
367
- #
368
- # @param properties [Hash] Tool schema properties
369
- # @return [Array<Symbol>]
370
- def integer_property_keys(properties)
371
- properties.select { |_k, v| v[:type] == 'integer' }.keys.map(&:to_sym)
207
+ spec.input_schema
372
208
  end
373
209
 
374
- # Build a SafeContext (Layer 3) from redaction settings, or nil when nothing is configured.
375
- #
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
376
236
  # @param redacted_columns [Array<String>]
377
237
  # @param redacted_key_values [Array<Hash>]
378
- # @return [SafeContext, nil]
379
- def build_safe_context(redacted_columns, redacted_key_values)
380
- return nil unless redacted_columns.any? || redacted_key_values.any?
381
-
382
- SafeContext.new(
383
- connection: nil,
384
- redacted_columns: redacted_columns,
385
- 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
386
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
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)]
387
273
  end
388
274
 
389
275
  # Bundle the three response-safety layers into a ResponseContext the
@@ -402,19 +288,19 @@ module Woods
402
288
  model_reflections: model_reflections)
403
289
  end
404
290
 
405
- scanner = build_scanner(config)
406
-
407
- ResponseContext.build(safe_ctx: safe_ctx, table_gate: table_gate, credential_scanner: scanner)
408
- end
409
-
410
- def build_scanner(config)
291
+ secret_index = build_credential_index(config)
411
292
  disabled_patterns = Array(config&.console_disabled_scanner_patterns)
412
- return nil if disabled_patterns.include?(:all)
293
+ scanner = if disabled_patterns.include?(:all)
294
+ nil
295
+ else
296
+ CredentialScanner.new(
297
+ disabled_patterns: disabled_patterns,
298
+ secret_index: secret_index
299
+ )
300
+ end
301
+ @active_scanner = scanner
413
302
 
414
- SCANNERS.register do
415
- CredentialScanner.new(disabled_patterns: disabled_patterns,
416
- secret_index: build_credential_index(config))
417
- end
303
+ ResponseContext.build(safe_ctx: safe_ctx, table_gate: table_gate, credential_scanner: scanner)
418
304
  end
419
305
 
420
306
  # Build the boot-time credential index from Rails.application's encrypted
@@ -469,40 +355,23 @@ module Woods
469
355
 
470
356
  # Shared server construction used by both build() and build_embedded().
471
357
  #
472
- # @param conn_mgr [ConnectionManager, EmbeddedExecutor] Any object with send_request(Hash) -> Hash
358
+ # @param conn_mgr [EmbeddedExecutor] Request executor
473
359
  # @param ctx [ResponseContext, nil] Optional context bundling response-safety layers
360
+ # @param tool_names [Array<String>] Exact executable tool names to advertise
474
361
  # @return [MCP::Server]
475
- def build_server(conn_mgr, ctx)
362
+ def build_server(conn_mgr, ctx, tool_names:)
476
363
  server = ::MCP::Server.new(
477
364
  name: 'woods-console',
478
- 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
479
367
  )
480
368
 
481
369
  renderer = build_console_renderer
482
370
 
483
- register_tier1_tools(server, conn_mgr, ctx, renderer: renderer)
484
- register_tier2_tools(server, conn_mgr, ctx, renderer: renderer)
485
- register_tier3_tools(server, conn_mgr, ctx, renderer: renderer)
486
- register_tier4_tools(server, conn_mgr, ctx, renderer: renderer)
487
- server
488
- end
489
-
490
- # Loud multi-line banner surfaced to stderr when the opt-in flag is
491
- # recognised outside of production. Operators should see this every
492
- # boot so an accidentally-persistent env var cannot go unnoticed.
493
- #
494
- # @return [String]
495
- def unsafe_eval_banner
496
- <<~BANNER
497
-
498
- ================================================================================
499
- WOODS_CONSOLE_UNSAFE_EVAL IS SET
500
- console_eval is LIVE on this process. Every run goes through EvalGuard +
501
- Confirmation + SafeContext rollback + a wall-clock timeout and is recorded
502
- to the audit log. The flag refuses to boot in Rails.env.production?.
503
- If you did not mean to set this, unset the env var.
504
- ================================================================================
505
- 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)
506
375
  end
507
376
 
508
377
  def structured_logger