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
@@ -0,0 +1,829 @@
1
+ # Console MCP Server Setup
2
+
3
+ The Console MCP Server gives MCP-capable coding tools and agents live access to your Rails application: database counts, record lookups, and schema inspection. It does not expose job-monitoring tools in supported modes. Database work on the request's current connection is rolled back, subject to the side-effect limits documented below.
4
+
5
+ ## Transport options at a glance
6
+
7
+ | Option | How it works | When to use |
8
+ |--------|-------------|-------------|
9
+ | [Stdio via rake](#option-a-stdio-via-rake-recommended) | Rake task boots Rails, runs MCP in-process | Local dev, simplest setup |
10
+ | [Docker](#option-b-docker) | Same rake task, piped through `docker exec -i` | Docker/Compose environments |
11
+ | [HTTP Rack middleware](#option-c-http-rack-middleware) | Middleware mounts `/mcp/console` endpoint | Shared access, multiple clients |
12
+ | [Launcher wrapper](#option-d-launcher-wrapper) | Execs the embedded server directly, through Docker, or through SSH | Centralized process-launch config |
13
+
14
+ ---
15
+
16
+ ## Option A: Stdio via Rake (recommended)
17
+
18
+ The simplest setup. The `woods:console` rake task boots Rails, then starts the embedded MCP server using stdio transport. All queries run in-process via ActiveRecord, no separate bridge process needed.
19
+
20
+ ### Prerequisites
21
+
22
+ 1. `gem 'woods'` in your Gemfile
23
+ 2. `bundle install`
24
+ 3. Deliberately enable Console MCP in `config/initializers/woods.rb` after reviewing the trust boundary:
25
+
26
+ ```ruby
27
+ Woods.configure do |config|
28
+ config.console_mcp_enabled = true
29
+ config.console_mcp_token = ENV["WOODS_CONSOLE_MCP_TOKEN"]
30
+ end
31
+ ```
32
+
33
+ The stdio and Docker entry points exit with status 1 while this setting is false. Enabling it grants the MCP process live read access under the blocked-table, redaction, and credential-scanning controls described below.
34
+
35
+ The token authenticates HTTP requests; a stdio client does not send it. Production Rails boot nevertheless requires a token of at least 32 characters whenever Console MCP is enabled, even for a stdio-only setup. Store `WOODS_CONSOLE_MCP_TOKEN` in the application's normal secret store. Outside production a missing token warns and leaves Console HTTP guarded with 401 — the boot warning names both transports, so a stdio-only setup can tell that the 401 is not its symptom and that its own session still works.
36
+
37
+ ### How it works
38
+
39
+ The rake task does two things before starting the MCP server:
40
+
41
+ 1. **Captures stdout before Rails boots.** Rails boot emits OpenTelemetry warnings, gem notices, and other output to stdout. An MCP client cannot parse these as JSON-RPC, they break the protocol. The rake task redirects stdout → stderr immediately, saves the real stdout fd, and restores it after boot completes.
42
+ 2. **Calls `Rails.application.eager_load!`** to load all application models. Without eager loading, only the models that happen to be autoloaded before the first query appear in the registry.
43
+
44
+ ### MCP client configuration
45
+
46
+ Add this server entry to your MCP client's project configuration:
47
+
48
+ ```json
49
+ {
50
+ "mcpServers": {
51
+ "rails-console": {
52
+ "command": "bundle",
53
+ "args": ["exec", "rake", "woods:console"],
54
+ "cwd": "/path/to/your/rails-app"
55
+ }
56
+ }
57
+ }
58
+ ```
59
+
60
+ ### What happens under the hood
61
+
62
+ ```
63
+ MCP client
64
+
65
+ │ spawns via stdio
66
+
67
+
68
+ rake woods:console
69
+
70
+ ├─ capture $stdout before boot
71
+ ├─ Rake::Task[:environment].invoke (Rails boots)
72
+ ├─ load exe/woods-console
73
+ │ ├─ Rails.application.eager_load!
74
+ │ ├─ build model registry from ActiveRecord::Base.descendants
75
+ │ ├─ Server.build_embedded(model_validator:, safe_context:, ...)
76
+ │ └─ MCP::Server::Transports::StdioTransport.new(server).open
77
+
78
+ └─ MCP server responds to tool calls via stdin/stdout
79
+ ```
80
+
81
+ Like the HTTP path, the stdio server hands `SafeContext` the connection
82
+ *pool*, so every tool call leases a fresh connection and returns it when the
83
+ rolled-back transaction ends. A long-lived stdio session therefore survives a
84
+ database failover or a `wait_timeout` recycle instead of failing every call on
85
+ a stale connection until the client restarts it.
86
+
87
+ ---
88
+
89
+ ## Option B: Docker
90
+
91
+ Same embedded approach as Option A, but piped through `docker exec -i`. The `-i` flag keeps stdin open for the MCP protocol. The container must be running before the MCP client starts.
92
+
93
+ ### Prerequisites
94
+
95
+ - Running container with Rails app
96
+ - `woods` gem in the container's Gemfile
97
+
98
+ ### MCP client configuration
99
+
100
+ **Plain Docker:**
101
+
102
+ ```json
103
+ {
104
+ "mcpServers": {
105
+ "rails-console": {
106
+ "command": "docker",
107
+ "args": [
108
+ "exec", "-i",
109
+ "your_app_web_1",
110
+ "bundle", "exec", "rake", "woods:console"
111
+ ]
112
+ }
113
+ }
114
+ }
115
+ ```
116
+
117
+ **Docker Compose** (when the service name is `web`):
118
+
119
+ ```json
120
+ {
121
+ "mcpServers": {
122
+ "rails-console": {
123
+ "command": "docker",
124
+ "args": [
125
+ "compose", "exec", "-T", "web",
126
+ "bundle", "exec", "rake", "woods:console"
127
+ ],
128
+ "cwd": "/absolute/host/path/to/app"
129
+ }
130
+ }
131
+ }
132
+ ```
133
+
134
+ > **Note:** Compose uses the service name and `-T` to disable its pseudo-TTY. Plain `docker exec` uses the exact container name from `docker ps` and needs `-i` to keep stdin open.
135
+
136
+ ### Environment variables
137
+
138
+ If your Rails app requires environment variables at boot (credentials, database URL), pass them via `docker exec -e` or ensure they are set in the container already:
139
+
140
+ ```json
141
+ {
142
+ "mcpServers": {
143
+ "rails-console": {
144
+ "command": "docker",
145
+ "args": [
146
+ "exec", "-i",
147
+ "-e", "RAILS_ENV=development",
148
+ "your_app_web_1",
149
+ "bundle", "exec", "rake", "woods:console"
150
+ ]
151
+ }
152
+ }
153
+ }
154
+ ```
155
+
156
+ ---
157
+
158
+ ## Option C: HTTP rack middleware
159
+
160
+ Mount the console as a Rack middleware endpoint. The MCP client connects over HTTP using the streamable-http transport instead of spawning a subprocess. Useful when multiple clients need shared access, or when stdio subprocess spawning is not practical.
161
+
162
+ ### Prerequisites
163
+
164
+ 1. `gem 'woods'` in Gemfile
165
+ 2. `bundle install`
166
+ 3. A running Rails server accessible to the MCP client
167
+
168
+ ### Rails configuration
169
+
170
+ In an initializer (`config/initializers/woods.rb`):
171
+
172
+ ```ruby
173
+ Woods.configure do |config|
174
+ config.console_mcp_enabled = true
175
+ config.console_mcp_token = ENV.fetch('WOODS_CONSOLE_MCP_TOKEN')
176
+ config.console_mcp_allowed_origins = [
177
+ 'https://rails.internal.example', # public Rails/MCP Host
178
+ 'https://agent.example' # browser client Origin, when applicable
179
+ ]
180
+ config.console_redacted_columns =
181
+ Woods::DEFAULT_CONSOLE_REDACTED_COLUMNS + %w[ssn]
182
+ end
183
+ ```
184
+
185
+ Set `WOODS_CONSOLE_MCP_TOKEN` to a random value of at least 32 characters in
186
+ the Rails server environment. The middleware stack registers automatically via
187
+ the gem's Railtie and requires `Authorization: Bearer <token>` on every Console
188
+ request. Missing or incorrect tokens receive `401 Unauthorized`.
189
+
190
+ For non-loopback access, `console_mcp_allowed_origins` must include the public
191
+ Rails/MCP host. If a browser-based client sends an `Origin` header from a
192
+ different host, include that exact origin too. This allow-list controls both
193
+ DNS-rebinding Host checks and browser CORS; keep Rails' own `config.hosts`, TLS,
194
+ and proxy rules aligned with it. Server-to-server clients normally omit
195
+ `Origin`, but their request `Host` must still be allowed.
196
+
197
+ Do not mount `Woods::Console::RackMiddleware` by itself. The Railtie composes
198
+ `OriginGuard`, `BearerAuth`, and the Console middleware in the supported order.
199
+
200
+ Streamable HTTP is stateless by default. For a legacy client that still
201
+ requires MCP session IDs, a custom guarded mount may pass `stateless: false`;
202
+ the default Railtie mount remains stateless.
203
+
204
+ ### MCP client configuration
205
+
206
+ For an MCP client that supports Streamable HTTP:
207
+
208
+ ```json
209
+ {
210
+ "mcpServers": {
211
+ "rails-console": {
212
+ "type": "streamable-http",
213
+ "url": "http://localhost:3000/mcp/console",
214
+ "headers": {
215
+ "Authorization": "Bearer <same WOODS_CONSOLE_MCP_TOKEN value>"
216
+ }
217
+ }
218
+ }
219
+ }
220
+ ```
221
+
222
+ For production or staging, use HTTPS in addition to the mandatory bearer token.
223
+
224
+ ### What happens under the hood
225
+
226
+ The middleware lazy-initializes the MCP server on first request:
227
+
228
+ ```
229
+ First HTTP request to /mcp/console
230
+
231
+ ├─ mutex-locked initialization
232
+ │ ├─ Rails.application.eager_load!
233
+ │ ├─ build model registry from ActiveRecord::Base.descendants
234
+ │ └─ Server.build_embedded(...)
235
+ │ └─ StreamableHTTPTransport wraps the server
236
+
237
+ └─ subsequent requests: transport.handle_request(rack_request)
238
+ ```
239
+
240
+ Each request gets its own database connection from the connection pool. `SafeContext` wraps that connection in a rolled-back transaction.
241
+
242
+ ### Security note
243
+
244
+ The HTTP endpoint grants read access to live database data. In production environments:
245
+
246
+ - Restrict the path to internal networks or authenticated users
247
+ - Use `console_redacted_columns` to redact sensitive fields (see [Configuration Options](#configuration-options))
248
+ - Consider mounting only in `development` and `staging` environments
249
+
250
+ ---
251
+
252
+ ## Option D: Launcher wrapper
253
+
254
+ `woods-console-mcp` is a process launcher. It replaces itself with the same
255
+ embedded server used by Options A-C, either directly or through `docker exec`
256
+ or `ssh`. It does not enable additional tool tiers.
257
+
258
+ This option requires the Woods gem and `woods-console-mcp` executable on the
259
+ host that runs the MCP client. If Woods exists only inside a container, use
260
+ [Option B](#option-b-docker) instead. When Woods is in a host application
261
+ bundle rather than installed as a standalone executable, launch it through
262
+ that bundle and set the application directory as `cwd`:
263
+
264
+ ```json
265
+ {
266
+ "command": "bundle",
267
+ "args": ["exec", "woods-console-mcp"],
268
+ "cwd": "/absolute/host/path/to/app"
269
+ }
270
+ ```
271
+
272
+ ### How it works
273
+
274
+ ```
275
+ MCP client
276
+
277
+ ├─ spawns: woods-console-mcp (reads console.yml)
278
+
279
+
280
+ ConnectionManager
281
+
282
+ └─ exec direct / docker exec -i / ssh
283
+ └─ bundle exec rake woods:console
284
+ ```
285
+
286
+ ### Configuration
287
+
288
+ Create `~/.woods/console.yml` (or point `WOODS_CONSOLE_CONFIG` to any YAML file):
289
+
290
+ Direct:
291
+
292
+ ```yaml
293
+ mode: direct
294
+ directory: /path/to/rails-app
295
+ command: bundle exec rake woods:console
296
+ ```
297
+
298
+ Docker:
299
+
300
+ ```yaml
301
+ mode: docker
302
+ container: my_app_web_1
303
+ command: bundle exec rake woods:console
304
+ ```
305
+
306
+ SSH:
307
+
308
+ ```yaml
309
+ mode: ssh
310
+ host: app.example.com
311
+ user: deploy
312
+ command: cd /app && bundle exec rake woods:console
313
+ ```
314
+
315
+ Override config path with environment variable:
316
+
317
+ ```bash
318
+ WOODS_CONSOLE_CONFIG=/path/to/console.yml woods-console-mcp
319
+ ```
320
+
321
+ ### MCP client configuration
322
+
323
+ ```json
324
+ {
325
+ "mcpServers": {
326
+ "rails-console": {
327
+ "command": "woods-console-mcp",
328
+ "env": {
329
+ "WOODS_CONSOLE_CONFIG": "/path/to/console.yml"
330
+ }
331
+ }
332
+ }
333
+ }
334
+ ```
335
+
336
+ > **Tier support:** Every launcher target exposes the embedded surface: 9 Tier 1 tools by default, or 11 tools when `console_sql` and `console_query` are explicitly enabled.
337
+
338
+ ---
339
+
340
+ ## Tool support by mode
341
+
342
+ The codebase keeps schemas for 31 possible tools as an inventory. Supported
343
+ servers register only executable tools: the 9 Tier 1 tools by default, plus
344
+ `console_sql` and `console_query` when `embedded_read_tools: true` (or
345
+ `config.console_embedded_read_tools = true`). Tier 2, Tier 3, and
346
+ `console_eval` are not registered in any supported mode.
347
+
348
+ ### Tier 1: Read-only (9 tools): supported in all modes
349
+
350
+ | Tool | Description |
351
+ |------|-------------|
352
+ | `console_status` | Health check: available models and database adapter |
353
+ | `console_schema` | Column types, nullability, defaults; optionally includes indexes |
354
+ | `console_count` | Record count matching scope conditions |
355
+ | `console_sample` | Random sample of records (max 25) |
356
+ | `console_find` | Find a record by primary key or unique column |
357
+ | `console_pluck` | Extract column values with optional distinct (max 1000 rows) |
358
+ | `console_aggregate` | Run `sum`, `average`, `minimum`, `maximum`, or `count` on a column (column optional for `count`) |
359
+ | `console_association_count` | Count associated records for a specific record |
360
+ | `console_recent` | Recently created/updated records (max 50) |
361
+
362
+ ### Tier 2: Domain-aware (9 tools): inventory only, not executable
363
+
364
+ | Tool | Description |
365
+ |------|-------------|
366
+ | `console_diagnose_model` | Full model diagnostic: counts, recent records, aggregates |
367
+ | `console_data_snapshot` | Record with associations for debugging (depth 1–3) |
368
+ | `console_validate_record` | Run validations on an existing record with optional changes |
369
+ | `console_validate_with` | Validate attributes against a model without persisting |
370
+ | `console_check_setting` | Check a configuration setting value |
371
+ | `console_update_setting` | Update a setting (requires confirmation) |
372
+ | `console_check_policy` | Check authorization policy for a record and user |
373
+ | `console_check_eligibility` | Check feature eligibility for a record |
374
+ | `console_decorate` | Invoke a decorator and return computed attributes |
375
+
376
+ ### Tier 3: Analytics (10 tools): Inventory only, not executable
377
+
378
+ | Tool | Description |
379
+ |------|-------------|
380
+ | `console_slow_endpoints` | Slowest endpoints by response time |
381
+ | `console_error_rates` | Error rates by controller or overall |
382
+ | `console_throughput` | Request throughput over time |
383
+ | `console_job_queues` | Job queue statistics |
384
+ | `console_job_failures` | Recent job failures |
385
+ | `console_job_find` | Find a job by ID, optionally retry (requires confirmation) |
386
+ | `console_job_schedule` | Scheduled/upcoming jobs |
387
+ | `console_redis_info` | Redis server information by section |
388
+ | `console_cache_stats` | Cache store statistics |
389
+ | `console_channel_status` | ActionCable channel status |
390
+
391
+ ### Tier 4: Guarded (3 tools): Read tools opt-in; eval inventory only
392
+
393
+ | Tool | Description |
394
+ |------|-------------|
395
+ | `console_eval` | Inventory schema only; not registered by supported modes |
396
+ | `console_sql` | Execute read-only SQL, `SELECT` and `WITH...SELECT` only |
397
+ | `console_query` | Enhanced query builder with joins, grouping, and HAVING |
398
+
399
+ `console_sql` and `console_query` register with `embedded_read_tools: true`.
400
+ `console_eval` remains unavailable; legacy unsafe-eval settings fail closed at boot.
401
+
402
+ ---
403
+
404
+ ## Configuration options
405
+
406
+ Set these in your Rails initializer:
407
+
408
+ ```ruby
409
+ Woods.configure do |config|
410
+ # Master on/off switch for the Console MCP feature (Layer 0). Default: false.
411
+ # Applies to every transport: stdio, launcher wrapper, and Rack.
412
+ # When false, stdio entry points exit with a "disabled" notice and the Rack
413
+ # middleware passes the request through to the host app untouched (the
414
+ # console path is indistinguishable from an unknown route). Set to true only
415
+ # after configuring the layers below that match your threat model.
416
+ config.console_mcp_enabled = true
417
+
418
+ # URL path for the Rack middleware endpoint. Default: '/mcp/console'.
419
+ config.console_mcp_path = '/mcp/console'
420
+
421
+ # HTTP Origin + Host allow-list. Defaults to loopback only. Non-loopback
422
+ # Rack deployments must include their public MCP host; browser clients from
423
+ # another origin need that exact origin listed too.
424
+ config.console_mcp_allowed_origins = [
425
+ 'https://rails.internal.example',
426
+ 'https://agent.example'
427
+ ]
428
+
429
+ # Layer 1. Table names that must never appear in a response.
430
+ # Default: Woods::DEFAULT_CONSOLE_BLOCKED_TABLES (8 tables, see below).
431
+ # Matched against :model (resolved via ActiveRecord), :table, and :sql args.
432
+ # A blocked table rejects the tool call at dispatch, before the executor runs.
433
+ # Case-insensitive.
434
+ config.console_blocked_tables = Woods::DEFAULT_CONSOLE_BLOCKED_TABLES + %w[authorizations]
435
+
436
+ # Layer 2. Content-shape credential scanner. Walks the final response tree
437
+ # and replaces credential-shaped substrings (Stripe sk_*, AWS AKIA*, GCP
438
+ # private keys, GitHub ghp_*, generic high-entropy tokens) with "[REDACTED]".
439
+ # Runs regardless of column naming, so it catches leaks that column-based
440
+ # redaction alone would miss. Default: [] (every pattern active).
441
+ #
442
+ # Accepts a list of pattern symbols to skip. Useful when a rule produces
443
+ # false positives in your data. Pass the :all sentinel to disable the
444
+ # scanner entirely (no layer-2 processing runs).
445
+ config.console_disabled_scanner_patterns = %i[stripe_publishable_key]
446
+ # config.console_disabled_scanner_patterns = %i[all] # disable scanner entirely
447
+
448
+ # Layer 2 augmentation, boot-time credential index. Default: true.
449
+ # Woods::Console::CredentialIndex walks Rails.application.credentials.config
450
+ # once at server boot and substring-redacts those values from every MCP
451
+ # response, so credentials whose shape no scanner pattern recognizes
452
+ # (Twilio auth tokens, hand-rolled HMAC seeds, third-party webhook
453
+ # signing keys) are still caught when their exact contents appear.
454
+ # See "console_credential_defense_enabled" section below for scope and
455
+ # multi-DB caveats.
456
+ config.console_credential_defense_enabled = true
457
+
458
+ # Layer 3. Column names to redact from all query results.
459
+ # Default: Woods::DEFAULT_CONSOLE_REDACTED_COLUMNS (~30 credential-shaped
460
+ # columns, see below). Replaced with "[REDACTED]" in output.
461
+ config.console_redacted_columns = Woods::DEFAULT_CONSOLE_REDACTED_COLUMNS + %w[ssn]
462
+
463
+ # Layer 3, EAV (key-value) redaction patterns. Default: [].
464
+ # See `console_redacted_key_values` section below for the pattern contract.
465
+ config.console_redacted_key_values = [
466
+ { key_column: 'key', value_column: 'value',
467
+ sensitive_keys: %w[stripe_access_token oauth_token] }
468
+ ]
469
+
470
+ # Unlock console_sql / console_query in the embedded executor. Default: false.
471
+ # Flows through to the Rack middleware AND the stdio entry point (rake / rails runner).
472
+ # See "Unlocking console_sql / console_query in embedded mode" below.
473
+ config.console_embedded_read_tools = false
474
+ end
475
+ ```
476
+
477
+ ### `console_mcp_enabled` (layer 0: feature gate)
478
+
479
+ Until this flag is `true`, none of the transports route traffic:
480
+
481
+ - `exe/woods-console` prints a notice to stderr and exits 1. `exe/woods-console-mcp` execs that target, so MCP clients see the same startup failure.
482
+ - `Woods::Console::RackMiddleware` passes the request through to the host app (typically its 404), so a disabled console path is indistinguishable from an unknown route. Non-matching paths always pass through untouched.
483
+
484
+ Keep the flag off in environments where the Console isn't needed (production web tier, CI). Flip it on per-environment, e.g. in `config/environments/development.rb` or a staging-only initializer, once the layers below are configured for that environment's threat model.
485
+
486
+ ### `console_blocked_tables` (layer 1: table gate)
487
+
488
+ Entries are lowercased table names. A tool call is rejected at dispatch time when:
489
+
490
+ - `:model` argument resolves to a model whose `table_name` is blocked
491
+ - `:table` argument names a blocked table
492
+ - `:sql` argument references a blocked table (matched on identifier tokens, case-insensitive)
493
+
494
+ **Ships with a curated default list** (`Woods::DEFAULT_CONSOLE_BLOCKED_TABLES`, 8 tables) covering common auth/credential storage across Devise, Doorkeeper, Rodauth, Sorcery, OmniAuth, and hand-rolled token systems: `sessions`, `api_keys`, `credentials`, `oauth_applications`, `oauth_access_tokens`, `oauth_refresh_tokens`, `identities`, `active_storage_blobs`.
495
+
496
+ `users` / `accounts` are intentionally excluded, many apps expose safe columns from these and should decide explicitly, as are PII-heavy but auth-unrelated tables (`payments`, `addresses`), which are an org-specific compliance concern.
497
+
498
+ Extend or override rather than reassigning blindly:
499
+
500
+ ```ruby
501
+ # Extend: keep all defaults plus app-specific tables
502
+ config.console_blocked_tables = Woods::DEFAULT_CONSOLE_BLOCKED_TABLES + %w[authorizations settings]
503
+
504
+ # Remove a default that over-blocks in your app
505
+ config.console_blocked_tables = Woods::DEFAULT_CONSOLE_BLOCKED_TABLES - %w[active_storage_blobs]
506
+
507
+ # Replace entirely: only do this if you've audited the default list against your schema
508
+ config.console_blocked_tables = %w[only_this]
509
+
510
+ # Disable Layer 1 entirely (other layers still apply)
511
+ config.console_blocked_tables = []
512
+ ```
513
+
514
+ Use this to wall off tables that shouldn't appear in agent context regardless of redaction posture, EAV credential stores, audit logs with full request bodies, or PII stores with legal access restrictions. Rejection is observable via the `console.table_gate.rejected` structured log line.
515
+
516
+ ### `console_disabled_scanner_patterns` (layer 2: content scanner)
517
+
518
+ The scanner runs after Layer 3 redaction, so it catches credential shapes that column and EAV patterns miss, e.g. a Stripe key pasted into a free-text `note` field, a JWT returned from a custom SQL query, or an access token logged by a callback. See `lib/woods/console/credential_scanner.rb` for the full rule list.
519
+
520
+ Scanner hits emit a `console.credential_scan.hits` warn-level structured log line with per-pattern counts, so you can audit how often the net fires in practice. Prefer fixing the upstream cause (moving the secret out of the leaking column) over disabling a rule, per-pattern opt-outs are an escape hatch, not a primary knob.
521
+
522
+ Setting `console_disabled_scanner_patterns = %i[all]` disables the entire scanner. No layer-2 processing runs. Layer 3 (column + EAV redaction) and Layer 4 (SqlValidator + SafeContext) continue to fire. Use this only when the pattern scanner interferes with a legitimate workflow and the remaining layers cover the threat model; prefer a per-pattern opt-out otherwise.
523
+
524
+ ### `console_credential_defense_enabled` (boot-time credential index)
525
+
526
+ `Woods::Console::CredentialIndex` walks `Rails.application.credentials.config`
527
+ once at server boot, collects string leaves with length >= 12, and
528
+ substring-redacts those values from every MCP response. `console_eval` is not
529
+ registered in a supported mode, so this setting does not enable eval.
530
+
531
+ **Restart required after credential rotation.** The index is built once at process start and held in memory for the lifetime of the MCP process. When a host app rotates Rails credentials (`rails credentials:edit`), the MCP process keeps the pre-rotation secrets in its Set until the process is restarted, new secrets are not picked up automatically. Only the Layer 2 shape-pattern scanner (Stripe `sk_*`, AWS `AKIA*`, etc.) can catch newly-rotated values before restart.
532
+
533
+ **Rebuild hook for rotation jobs.** If you rotate credentials from a Rake task or a deployment hook and want to avoid a full restart, call:
534
+
535
+ ```ruby
536
+ Woods::Console::Server.rebuild_credential_index(rails_app: Rails.application)
537
+ ```
538
+
539
+ This rebuilds the index from the current credentials and hot-swaps it into the active scanner. The swap is atomic on MRI, in-flight scans see either the old or the new index, never a partial one. The method is a no-op (returns `nil`) when no server has been built yet or when `console_credential_defense_enabled` is `false`.
540
+
541
+ **Rotation warning.** At boot time, Woods checks whether any credentials file (`config/credentials.yml.enc`, `config/credentials/<env>.yml.enc`) was modified *after* the process started. When it detects this, it emits a `console.credential_index.stale` warn-level structured log line with the file path, mtime, and a hint to restart or call `rebuild_credential_index`. This check is on by default; disable it with:
542
+
543
+ ```ruby
544
+ config.console_credential_rotation_warning = false
545
+ ```
546
+
547
+ **Multi-DB / sharded caveat.** The index reflects only the credentials available to the *Rails process* that boots the Console MCP server. A separate database that holds its own secrets (e.g., a vendored CMS app sharing the same Rails host) is not in scope, for those, lean on Layer 3 (`console_redacted_columns` / `console_redacted_key_values`) and Layer 1 (`console_blocked_tables`).
548
+
549
+ **Missing master key.** In environments without `config/master.key` (CI, fresh checkouts), the index build catches `MissingKeyError` / `InvalidMessage` by class name and returns an empty index. The server still boots and the configured table, scanner, and redaction layers remain in effect.
550
+
551
+ Set the flag to `false` only when the host intentionally opts out of exact-value credential indexing and the remaining scanner/redaction layers cover its response data.
552
+
553
+ ### `console_redacted_columns`
554
+
555
+ Redaction replaces matching column values with `"[REDACTED]"` before the MCP response is sent. Column names are matched by string, case-sensitive, use the exact names from your database schema.
556
+
557
+ Redacted columns are also **refused as query inputs**: as the `column` of `console_aggregate`, as a scope or `by:` key (including `_matches` forms), and as an order or grouping key. EAV `value_column` entries from `console_redacted_key_values` are refused in those predicate, ordering, and grouping positions for the same reason; EAV `key_column` entries remain valid predicates because they identify the row whose value should be masked. Aggregates refuse either column of an EAV pair. Masking output alone would leave a comparison, ordering, or aggregate oracle over the secret.
558
+
559
+ For `console_query`'s `select`, three expression shapes are additionally refused, because the redactor masks by output header name and these shapes rename or read the protected value:
560
+
561
+ - **An `AS` alias over a redacted column** (`password_digest AS note`) — the value would return plaintext under a header no redaction rule matches.
562
+ - **An aggregate over a redacted column, aliased or bare** (`SUM(salary)`, `SUM(salary) AS total`) — the aggregate itself is the secret's value. Aggregates over either column of a `console_redacted_key_values` pair are refused the same way.
563
+ - **An `AS` alias over either column of a `console_redacted_key_values` pair** — the positional EAV rule resolves key/value columns by header name, so renaming either header disarms it.
564
+ - **Any `AS` alias whose name collides with a protected header** (`id AS value` when `value` is an EAV value column) — the duplicate header could shadow the real column and steal its mask. Alias names are compared case-insensitively, matching unquoted SQL identifier folding.
565
+
566
+ Direct, unaliased selection of a redacted column stays allowed: the output header keeps the column's real name, and the positional redactor masks the value as usual. For an EAV pair, the **value column is only accepted when its paired key column is selected directly too** (that is what lets the positional rule mask it); selecting the value column alone is refused across `console_query`, `console_sample`, `console_find`, `console_pluck`, and `console_recent`.
567
+
568
+ `console_sql` applies a stricter form of the same rule because arbitrary SQL can rename output headers. A protected identifier is accepted only as a direct, unaliased outer `SELECT` column; aliases, aggregates, predicates, CTE shapes, ordering/grouping uses, and an EAV value without its paired key are refused before adapter execution. Use `console_query` when a protected column must participate in a more complex structured query.
569
+
570
+ `console_query`'s `having` is covered by the same oracle rule: an aggregate over a protected column (`MAX(amount) > ?`), a bare predicate on a redacted column (`salary > ?`), or a predicate on an EAV value column is refused, since repeated guesses reveal the protected value from whether a row is returned. EAV key predicates stay allowed so callers can select the rows whose paired values need redaction.
571
+
572
+ **Ships with a curated credential default list** (`Woods::DEFAULT_CONSOLE_REDACTED_COLUMNS`, 31 columns) covering Devise, Doorkeeper, Rodauth, has_secure_password, devise-two-factor, and common hand-rolled auth shapes: `password`, `password_digest`, `password_salt`, `encrypted_password`, `crypted_password`, `salt`, `otp_secret`, `encrypted_otp_secret`, `two_factor_secret`, `backup_codes`, `consumed_timestep`, `reset_password_token`, `confirmation_token`, `unlock_token`, `remember_token`, `invitation_token`, `access_token`, `refresh_token`, `auth_token`, `api_token`, `api_key`, `bearer_token`, `client_secret`, `webhook_secret`, `signing_secret`, `session_secret`, `private_key`, `encrypted_private_key`, `key_hash`, `token`, `secret`.
573
+
574
+ Extend or override rather than reassigning blindly:
575
+
576
+ ```ruby
577
+ # Extend: keep all defaults plus app-specific columns
578
+ config.console_redacted_columns = Woods::DEFAULT_CONSOLE_REDACTED_COLUMNS + %w[cart_token share_token]
579
+
580
+ # Add PII on top of the credential defaults
581
+ config.console_redacted_columns = Woods::DEFAULT_CONSOLE_REDACTED_COLUMNS + %w[email phone_number ssn]
582
+
583
+ # Remove a default that over-redacts in your app (e.g., `token` is a non-secret slug)
584
+ config.console_redacted_columns = Woods::DEFAULT_CONSOLE_REDACTED_COLUMNS - %w[token]
585
+
586
+ # Replace entirely: only do this if you've audited the default list against your schema
587
+ config.console_redacted_columns = %w[password_digest api_key]
588
+ ```
589
+
590
+ Columns intentionally **excluded** from the default list because they cause over-redaction in apps that use them legitimately:
591
+
592
+ - `key`: ActiveStorage blob keys, EAV key columns, translation keys
593
+ - `name`: universal non-secret identifier
594
+ - PII columns (`ssn`, `tax_id`, `dob`), org-specific compliance concern, prefer explicit opt-in
595
+
596
+ Redaction is shape-aware and covers every tool that returns row data:
597
+
598
+ | Tool | Output shape | How redaction applies |
599
+ | ------------------------------------- | --------------------------------------------------------- | --------------------- |
600
+ | `console_find` | `{record: Hash}` | Redacted column keys are replaced inside the nested record |
601
+ | `console_sample`, `console_recent` | `{records: [Hash, ...]}` | Each record hash is redacted |
602
+ | `console_sql`, `console_query` | `{columns: [...], rows: [[...], ...], count: N}` | Positional, rows are redacted by matching the `columns` header |
603
+ | `console_pluck` | `{columns: [...], values: [[...], ...]}` or `{values: [...]}` for a single column | Positional, multi-column rows and flat single-column arrays both covered |
604
+ | `console_count`, `console_aggregate`, `console_association_count`, `console_schema` | No row data | Nothing to redact |
605
+
606
+ Redaction is defense-in-depth, prefer not storing plaintext secrets in database columns in the first place, but it keeps configured credential columns out of the agent's transcript when `console_sample`, `console_find`, or the Tier 4 read tools return matching rows.
607
+
608
+ ### `console_redacted_key_values`
609
+
610
+ Column-name redaction falls short when credentials are stored in a **key-value (EAV)** table, e.g. a Stripe Connect `authorizations` row of `{key: "stripe_access_token", value: "sk_live_..."}`. The column holding the secret is called `value`, which is generic: adding `value` to `console_redacted_columns` would over-redact every unrelated row in the table.
611
+
612
+ `console_redacted_key_values` takes one or more patterns that describe "when a row has `key_column` set to one of these names, redact its `value_column`":
613
+
614
+ ```ruby
615
+ # Example: an `authorizations` table: pattern works on both MySQL and PostgreSQL.
616
+ config.console_redacted_key_values = [
617
+ {
618
+ key_column: 'key',
619
+ value_column: 'value',
620
+ sensitive_keys: %w[stripe_access_token stripe_publishable_key stripe_user_id
621
+ oauth_token refresh_token client_secret]
622
+ }
623
+ ]
624
+ ```
625
+
626
+ ```ruby
627
+ # An app with a generic `settings` table on MySQL or PostgreSQL uses a different
628
+ # column layout: patterns stack without interfering.
629
+ config.console_redacted_key_values = [
630
+ { key_column: 'name', value_column: 'value',
631
+ sensitive_keys: %w[smtp_password slack_webhook_url] },
632
+ { key_column: 'key', value_column: 'value',
633
+ sensitive_keys: %w[stripe_access_token oauth_token] }
634
+ ]
635
+ ```
636
+
637
+ Behavior:
638
+
639
+ | Response shape | EAV redaction applies when |
640
+ | --------------------------------------------------------------------- | ------------------------------------------------------------------- |
641
+ | `console_find`, `{record: {..., key: ..., value: ...}}` | `record[key_column]` ∈ `sensitive_keys` → `record[value_column] = "[REDACTED]"` |
642
+ | `console_sample`, `console_recent`, `{records: [{key:, value:}, ...]}` | Per-row, each record is evaluated against every configured pattern |
643
+ | `console_sql`, `console_query`, `{columns: [...], rows: [[...]]}` | Positional, `key_column` and `value_column` resolved to indexes once, per row lookup afterwards |
644
+ | `console_pluck`, `{columns: [...], values: [[...]]}` | Same positional logic as `rows` |
645
+
646
+ A pattern is skipped silently when its `key_column` or `value_column` is absent from the current `columns` header, so unrelated queries pay nothing for the configuration. Comparison is case-sensitive and coerces the key cell through `to_s` before matching, so `:stripe_access_token` and `"stripe_access_token"` both fire.
647
+
648
+ `console_redacted_columns` and `console_redacted_key_values` run in a single pass, configure both for apps that store credentials in both dedicated columns (e.g. `crypted_password`) and EAV rows (e.g. `authorizations.value`).
649
+
650
+ ### Unlocking `console_sql` / `console_query`
651
+
652
+ All three embedded transports (Options A, B, C) and the launcher wrapper
653
+ (Option D) start the same embedded server, so they all read one setting:
654
+ `console_embedded_read_tools`. Default `false`, `console_sql` and
655
+ `console_query` return an `error_type: "unsupported"` refusal without ever
656
+ touching ActiveRecord, and neither is registered in `tools/list`.
657
+
658
+ ```ruby
659
+ # config/initializers/woods.rb
660
+ Woods.configure do |config|
661
+ config.console_mcp_enabled = true # mount the Rack middleware via Railtie
662
+ config.console_mcp_token = ENV.fetch('WOODS_CONSOLE_MCP_TOKEN')
663
+ config.console_embedded_read_tools = true # unlock console_sql / console_query
664
+ config.console_redacted_columns = Woods::DEFAULT_CONSOLE_REDACTED_COLUMNS
665
+ end
666
+ ```
667
+
668
+ With the flag on, every request through `console_sql` / `console_query` runs
669
+ these controls, in order:
670
+
671
+ 1. `SqlValidator` rejects DML/DDL (`INSERT`/`UPDATE`/`DELETE`/`MERGE`/`DROP`/`TRUNCATE`/`ALTER`/`CREATE`/`REPLACE`), row-lock clauses (`FOR UPDATE`, `FOR SHARE`, `LOCK IN SHARE MODE`), writable CTEs (every `AS (...)` body, not just the first), `UNION`/`INTO`/`COPY`, multi-statement and comment-hidden injections, and most administrative keywords (`DO`, `SET`, `LISTEN`, `NOTIFY`, `CALL`, `LOAD`, `VACUUM`, `PREPARE`, transaction control, `EXPLAIN ANALYZE`) at the string level. Enforces a read-only **function allowlist** (`ALLOWED_FUNCTIONS`), anything not on it is rejected by name, quoted forms (`"pg_terminate_backend"(…)`) included. Only `SELECT`, `WITH…SELECT`, and plain `EXPLAIN` pass.
672
+ 2. `TableGate` refuses any SQL, model, or join that touches a `console_blocked_tables` entry.
673
+ 3. `SafeContext` wraps every request in a rolled-back transaction with a short statement timeout. **It does NOT cover async side effects**: ActiveJob `perform_later`, ActionMailer `deliver_later`, direct HTTP egress, `Thread.new`-spawned work, `after_rollback` callbacks, and writes through a different shard all execute as live. Treat the Console MCP as an admin-trust boundary, not a sandbox.
674
+ 4. `CredentialScanner` + column/EAV redaction scrub results.
675
+
676
+ Keep the flag off when the host requires a narrower database capability.
677
+
678
+ ---
679
+
680
+ ## Safety model
681
+
682
+ The executable Console surface uses the following defense layers in every
683
+ supported transport (stdio, Docker/SSH launcher, and HTTP).
684
+
685
+ | # | Layer | Knob | Fires at | Purpose |
686
+ |---|-------|------|----------|---------|
687
+ | 0 | Feature gate | `console_mcp_enabled` | Process start / request entry | Master on/off switch, feature is inert until an operator opts in |
688
+ | 1 | Blocked tables | `console_blocked_tables` | Tool dispatch, before executor | Reject any tool call that touches a named table (model, table, or sql arg) |
689
+ | 2 | Credential scanner | `console_disabled_scanner_patterns` (`[:all]` to disable entirely) | After executor, before render | Content-shape redaction of credential-shaped strings anywhere in the response tree |
690
+ | 3 | Column + EAV redaction | `console_redacted_columns`, `console_redacted_key_values` | After executor, before Layer 2 | Identity-based redaction by column name and by key/value row shape |
691
+ | 4 | SqlValidator + SafeContext | built-in | Inside executor | SQL deny-list for `console_sql`; transaction-rollback for every request |
692
+
693
+ Layers 0–3 are configured via `Woods.configure`. Layer 4 is always on and has no knobs. Observability hooks, `console.table_gate.rejected` for Layer 1, `console.credential_scan.hits` for Layer 2, emit structured log lines via `Woods::Observability::StructuredLogger` so operators can audit enforcement without scraping MCP wire traffic.
694
+
695
+ ### Confirmation and audit inventory
696
+
697
+ No currently executable tool claims a confirmation or privileged audit-log
698
+ contract. Tier 2, Tier 3, and `console_eval` remain unregistered inventory.
699
+
700
+ ### Current-connection rollback
701
+
702
+ Database work performed through the request's current Active Record connection
703
+ runs inside a transaction that is **always rolled back**:
704
+
705
+ ```ruby
706
+ def with_rolled_back_transaction
707
+ @connection.transaction do
708
+ set_timeout # statement timeout before any query
709
+ yield # run the tool
710
+ raise ActiveRecord::Rollback # always roll back
711
+ end
712
+ end
713
+ ```
714
+
715
+ This means direct mutations on that connection are discarded. It is a final
716
+ guard for ordinary database work, not a sandbox or a universal side-effect
717
+ guarantee. It does **not** undo Active Job or mail delivery, HTTP/network calls,
718
+ threaded work, `after_rollback` callbacks, or writes through another connection
719
+ or shard. The validator, table gate, credential defenses, and operator trust
720
+ boundary remain necessary.
721
+
722
+ - Direct accidental mutation on the wrapped connection is rolled back.
723
+ - External, asynchronous, callback, and cross-connection effects may execute live.
724
+
725
+ ### Statement timeout
726
+
727
+ Each transaction sets a statement timeout before any query runs. The default is **5000ms** (5 seconds). Timeout enforcement is adapter-specific:
728
+
729
+ | Adapter | Mechanism | Scope |
730
+ |---------|-----------|-------|
731
+ | PostgreSQL | `SET statement_timeout = '5000ms'` | All statement types |
732
+ | MySQL | `SET max_execution_time = 5000` (session scope; the prior value is restored after the transaction) | SELECT only (MySQL limitation) |
733
+ | Other | Best-effort (skipped gracefully) | n/a |
734
+
735
+ ### SQL validation (tier 4 `console_sql`)
736
+
737
+ `SqlValidator` rejects non-read-only SQL at the string level, before any database interaction.
738
+
739
+ Validation runs **once**, inside the executor, with the dialect of the live adapter. There is deliberately no earlier dialect-blind pre-check in the tool handler: a validator built without a dialect is the conservative MySQL+PostgreSQL union, and running it first meant a MySQL host rejected statements whose `\'`/backtick grammar produces a spuriously forbidden PostgreSQL view — the adapter-aware acceptance below could never be reached on a real transport. The executor raises `SqlValidationError` for anything it refuses, which the dispatch pipeline renders as a tool error, so nothing is ungated.
740
+
741
+
742
+ - **Allowed prefixes:** `SELECT`, `WITH...SELECT`, and plain `EXPLAIN`. `EXPLAIN ANALYZE` is rejected, it executes the query rather than just planning it (both the whitespace and `EXPLAIN (ANALYZE, …)` option-list spellings).
743
+ - **Rejected prefixes:** `INSERT`, `UPDATE`, `DELETE`, `MERGE`, `DROP`, `ALTER`, `TRUNCATE`, `CREATE`, `GRANT`, `REVOKE`
744
+ - **Rejected anywhere in query:** `UNION`, `INTO`, `COPY`; row-lock clauses (`FOR UPDATE`, `FOR NO KEY UPDATE`, `FOR SHARE`, `FOR KEY SHARE`, `FOR UPDATE NOWAIT`/`SKIP LOCKED`, MySQL `LOCK IN SHARE MODE`) — these take live row locks even inside the rolled-back transaction. The lock check is adapter-aware: `console_sql` validates with the active adapter's dialect, including MySQL double-quoted strings/backtick identifiers and PostgreSQL quoted identifiers/E-strings. Unknown adapters conservatively scan both normalizations. Every view is scanned under both MySQL executable-comment (`/*!...*/`) semantics, so `#` comments and version-guarded comments cannot split a clause apart.
745
+ - **Function allowlist (the authoritative function control):** every function-call-shaped identifier must appear in `ALLOWED_FUNCTIONS`, a conservative set of pure read-only functions (aggregates, window functions, string/number/date/JSON readers) kept portable across MySQL, PostgreSQL, and SQLite. Anything else is rejected by name, quoted forms (`"pg_terminate_backend"(…)`) included. This is an allowlist because a denylist cannot enumerate every side-effecting function (`nextval`, `pg_advisory_lock`, `pg_terminate_backend`, …). A legacy `DANGEROUS_FUNCTIONS` denylist (`pg_sleep`, `lo_import`, `lo_export`, `pg_read_file`, `pg_write_file`, `load_file`, `sleep`, `benchmark`) still runs first as belt-and-suspenders.
746
+ - **Rejected patterns:** multiple statements (semicolons), writable CTEs (every `AS (...)` body is checked, so a writable CTE in any WITH position is refused — `WITH a AS (SELECT 1), b AS (DELETE FROM users RETURNING *) SELECT * FROM b`), a CTE list attached to top-level DML (`WITH a AS (SELECT 1) DELETE FROM users RETURNING *`), comment-hidden injections
747
+
748
+ ### Model and column validation
749
+
750
+ Before any query runs, the model name is checked against the registry built from `ActiveRecord::Base.descendants`. Unrecognized model names raise `ValidationError` without touching the database. Column names are validated against the model's `column_names` before pluck, aggregate, and recent operations.
751
+
752
+ For `console_query`, a schema-qualified column reference such as `orders.total` is validated for **ownership**: the table side is gated through `TableGate` (a blocked table is refused) and the column must actually exist on that table, so a blocked-table column cannot be smuggled through `select`, `order`, or `having`. Bare columns validate against the active model.
753
+
754
+ Scope hashes accept Ransack-style predicate suffixes (`_eq`, `_not_eq`, `_gt`, `_gteq`, `_lt`, `_lteq`, `_in`, `_not_in`, `_null`, `_not_null`, `_present`, `_blank`, `_matches`), see the [cookbook](MCP_TOOL_COOKBOOK.md#scope-predicates) for the full table. Every column name in a suffixed key is validated before an Arel predicate is built, so SQL injection via column names is not possible.
755
+
756
+ ---
757
+
758
+ ## Troubleshooting
759
+
760
+ ### MCP client shows no tools or "connection refused"
761
+
762
+ - **Rake/Docker:** Check that `cwd` in MCP config points to the Rails app root (where `Rakefile` lives).
763
+ - **HTTP:** Check that the Rails server is running and listening on the expected port. An unauthenticated `curl http://localhost:3000/mcp/console` should return `401` when the enabled middleware and bearer-auth guard are mounted. A request with the configured bearer token proceeds to MCP protocol handling.
764
+ - **All modes:** Run `bundle exec rake woods:console` directly in a terminal. It should hang (waiting for MCP protocol input) rather than exit immediately. If it exits, check the error output.
765
+
766
+ ### Rails boot noise breaks MCP protocol
767
+
768
+ The rake task redirects stdout to stderr before Rails boots specifically to prevent this. If you see JSON parse errors from the MCP client, check:
769
+
770
+ 1. You are using `bundle exec rake woods:console`, not `rails runner exe/woods-console` directly (the runner path handles this too, but via a different mechanism).
771
+ 2. No `puts` or `print` calls run at boot in your initializers before the task can capture stdout.
772
+ 3. Try running `bundle exec rake woods:console 2>/dev/null` to isolate, the MCP protocol output goes to stdout, Rails noise goes to stderr.
773
+
774
+ ### Models not visible to `console_status`
775
+
776
+ `console_status` returns the list of models registered at startup. If a model is missing:
777
+
778
+ 1. Check that it inherits from `ActiveRecord::Base` (not from an intermediate abstract class that doesn't itself inherit AR).
779
+ 2. Check that `model.table_exists?` returns true, models for tables that don't exist are excluded.
780
+ 3. Check that `eager_load!` succeeds. If your app has a directory that fails to load (e.g., `app/graphql/` requiring an uninstalled gem), Zeitwerk may abort early and skip models defined later alphabetically. Look for `NameError` in the boot output.
781
+
782
+ ### `console_sql` rejects my query
783
+
784
+ `SqlValidator` is conservative by design. If a valid read-only query is rejected:
785
+
786
+ - `UNION` in any position is blocked, use `console_query` with joins instead.
787
+ - Plain `EXPLAIN` is allowed; `EXPLAIN ANALYZE` is **rejected** because it executes the query rather than just planning it.
788
+ - A function is rejected unless it is on the read-only allowlist (`ALLOWED_FUNCTIONS`). If a legitimate pure/read function is missing, that is the list to extend, deliberately.
789
+ - Queries with semicolons are blocked even if the second statement is a comment, strip trailing semicolons.
790
+
791
+ ### A tool from the 31-schema inventory is not listed
792
+
793
+ See [Tool Support by Mode](#tool-support-by-mode): only 9 (or 11 with
794
+ `console_embedded_read_tools`) are ever registered. For `console_sql` /
795
+ `console_query`, see [Unlocking `console_sql` / `console_query`](#unlocking-console_sql--console_query). Tier 2, Tier 3, and `console_eval` are inventory-only in every mode, no flag registers them.
796
+
797
+ ### `console_eval` and `WOODS_CONSOLE_UNSAFE_EVAL`
798
+
799
+ `console_eval` is not registered by a supported server mode. Setting
800
+ `WOODS_CONSOLE_UNSAFE_EVAL=true`, enabling the equivalent configuration flag,
801
+ or passing the legacy confirmation/audit options causes server construction to
802
+ fail closed with `Woods::ConfigurationError`.
803
+
804
+ ### Slow first request on HTTP/Rack middleware
805
+
806
+ The middleware lazy-initializes the MCP server on the first request, which includes `Rails.application.eager_load!`. This can take several seconds on large apps. Subsequent requests are fast. If you want to pre-warm, call a health check endpoint that touches the middleware path at app startup.
807
+
808
+ ### Timeout errors on large models
809
+
810
+ The default statement timeout is 5000ms (5 seconds). If you are hitting timeouts on models with millions of rows, use `scope` to narrow the query:
811
+
812
+ ```
813
+ console_count(model: "Order", scope: { status: "pending" })
814
+ ```
815
+
816
+ The timeout is set per-transaction in `SafeContext` and is not currently configurable via `Woods.configure`. To change it, pass `timeout_ms:` to `SafeContext.new` directly if you are constructing the server programmatically.
817
+
818
+ ## Database dialect validation
819
+
820
+ Blocked-table matching normalizes qualified identifiers even when whitespace or
821
+ comments surround the schema separator. SQL validation uses the connected adapter's
822
+ quote and comment rules. MySQL also reads the executing session's `ANSI_QUOTES`
823
+ and `NO_BACKSLASH_ESCAPES` settings for validation, protected-column scanning, and
824
+ table gating; adjacent subtraction operators are not assumed to begin a comment.
825
+ Direct scanner callers without session settings use conservative quote-mode scans.
826
+ The contributor live-backend lane exercises these boundaries
827
+ through Console requests against PostgreSQL and MySQL. Keep read tools disabled
828
+ unless live SQL access is needed, and retain the configured blocked-table and
829
+ redaction policies when diagnosing a rejected request.