woods 2.0.0 → 2.1.0

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 (167) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +108 -0
  3. data/CONTRIBUTING.md +135 -10
  4. data/README.md +1 -1
  5. data/docs/AGENT_GUIDE.md +19 -0
  6. data/docs/AGENT_SETUP.md +22 -2
  7. data/docs/BACKEND_MATRIX.md +7 -0
  8. data/docs/CLIENT_HOOKS.md +6 -0
  9. data/docs/CONFIGURATION_REFERENCE.md +133 -18
  10. data/docs/CONSOLE_MCP_SETUP.md +141 -12
  11. data/docs/EMBEDDING_MODELS.md +16 -19
  12. data/docs/EXTRACTOR_REFERENCE.md +219 -21
  13. data/docs/FAQ.md +11 -25
  14. data/docs/GETTING_STARTED.md +7 -1
  15. data/docs/INCREMENTAL_EXTRACTION.md +261 -19
  16. data/docs/INDEX_LAYOUT.md +5 -0
  17. data/docs/INTERNALS.md +9 -0
  18. data/docs/MCP_HTTP_TRANSPORT.md +59 -2
  19. data/docs/MCP_SERVERS.md +87 -8
  20. data/docs/MCP_TOOL_COOKBOOK.md +13 -55
  21. data/docs/NOTION_INTEGRATION.md +7 -1
  22. data/docs/PUBLISHED_INDEX.md +6 -0
  23. data/docs/README.md +6 -1
  24. data/docs/RETRIEVAL_GUIDE.md +17 -0
  25. data/docs/SOURCE_FRESHNESS.md +157 -5
  26. data/docs/TOKEN_BENCHMARK.md +10 -18
  27. data/docs/TROUBLESHOOTING.md +70 -14
  28. data/docs/UNBLOCKED_INTEGRATION.md +60 -8
  29. data/docs/UPGRADING_TO_2.md +184 -9
  30. data/docs/WATCH_DAEMON.md +97 -14
  31. data/exe/woods-console-mcp +2 -2
  32. data/exe/woods-mcp-http +16 -9
  33. data/lib/generators/woods/templates/woods.rb.tt +2 -1
  34. data/lib/tasks/woods.rake +23 -7
  35. data/lib/tasks/woods_checks.rake +2 -2
  36. data/lib/woods/agent_configuration/cli.rb +1 -1
  37. data/lib/woods/agent_configuration/layout.rb +16 -2
  38. data/lib/woods/agent_configuration/plan.rb +13 -3
  39. data/lib/woods/agent_configuration/planner_validation.rb +4 -2
  40. data/lib/woods/agent_configuration/preflight.rb +5 -3
  41. data/lib/woods/builder.rb +17 -57
  42. data/lib/woods/cache/cache_middleware.rb +68 -41
  43. data/lib/woods/chunking/contributor_chunks.rb +119 -0
  44. data/lib/woods/chunking/semantic_chunker.rb +44 -21
  45. data/lib/woods/console/adapter_family.rb +39 -0
  46. data/lib/woods/console/connection_manager.rb +56 -3
  47. data/lib/woods/console/credential_index.rb +33 -3
  48. data/lib/woods/console/embedded_executor.rb +431 -48
  49. data/lib/woods/console/model_validator.rb +8 -0
  50. data/lib/woods/console/rack_middleware.rb +68 -11
  51. data/lib/woods/console/redactor.rb +24 -10
  52. data/lib/woods/console/safe_context.rb +44 -7
  53. data/lib/woods/console/sql_noise_stripper.rb +41 -12
  54. data/lib/woods/console/sql_table_scanner.rb +45 -34
  55. data/lib/woods/console/sql_validator.rb +37 -2
  56. data/lib/woods/dependency_graph.rb +34 -10
  57. data/lib/woods/embedding/fake.rb +12 -0
  58. data/lib/woods/embedding/indexer.rb +195 -98
  59. data/lib/woods/embedding/input_budget.rb +67 -0
  60. data/lib/woods/embedding/openai.rb +70 -20
  61. data/lib/woods/embedding/provider.rb +37 -25
  62. data/lib/woods/embedding/text_preparer.rb +76 -32
  63. data/lib/woods/embedding/token_counter.rb +18 -81
  64. data/lib/woods/embedding/vector_configuration.rb +48 -0
  65. data/lib/woods/extraction_identities.rb +175 -0
  66. data/lib/woods/extractor.rb +304 -107
  67. data/lib/woods/extractors/action_cable_extractor.rb +8 -3
  68. data/lib/woods/extractors/assigned_value_discovery.rb +74 -0
  69. data/lib/woods/extractors/class_declarations.rb +121 -0
  70. data/lib/woods/extractors/configuration_extractor.rb +11 -3
  71. data/lib/woods/extractors/declaration_ancestry.rb +92 -0
  72. data/lib/woods/extractors/event_extractor.rb +8 -0
  73. data/lib/woods/extractors/graphql_extractor.rb +134 -77
  74. data/lib/woods/extractors/job_extractor.rb +5 -1
  75. data/lib/woods/extractors/lib_extractor.rb +132 -15
  76. data/lib/woods/extractors/mailer_extractor.rb +3 -5
  77. data/lib/woods/extractors/manager_extractor.rb +7 -21
  78. data/lib/woods/extractors/migration_declaration.rb +87 -0
  79. data/lib/woods/extractors/migration_extractor.rb +5 -39
  80. data/lib/woods/extractors/phlex_extractor.rb +6 -2
  81. data/lib/woods/extractors/policy_extractor.rb +9 -5
  82. data/lib/woods/extractors/poro_extractor.rb +112 -53
  83. data/lib/woods/extractors/pundit_extractor.rb +11 -6
  84. data/lib/woods/extractors/scheduled_job_extractor.rb +45 -4
  85. data/lib/woods/extractors/serializer_extractor.rb +34 -22
  86. data/lib/woods/extractors/shared_utility_methods.rb +18 -1
  87. data/lib/woods/extractors/source_nesting.rb +142 -106
  88. data/lib/woods/extractors/standalone_module_discovery.rb +123 -0
  89. data/lib/woods/extractors/state_machine_extractor.rb +46 -40
  90. data/lib/woods/extractors/view_component_extractor.rb +9 -7
  91. data/lib/woods/flow_assembler.rb +4 -1
  92. data/lib/woods/generation.rb +25 -0
  93. data/lib/woods/hooks/context_hint.rb +7 -2
  94. data/lib/woods/mcp/bearer_auth.rb +1 -1
  95. data/lib/woods/mcp/bootstrapper.rb +33 -7
  96. data/lib/woods/mcp/config_resolver.rb +26 -7
  97. data/lib/woods/mcp/index_reader.rb +125 -24
  98. data/lib/woods/mcp/index_reader_pinning.rb +16 -0
  99. data/lib/woods/mcp/origin_guard.rb +24 -77
  100. data/lib/woods/mcp/origin_policy.rb +124 -0
  101. data/lib/woods/mcp/renderers/markdown_renderer.rb +7 -1
  102. data/lib/woods/mcp/renderers/plain_renderer.rb +3 -1
  103. data/lib/woods/mcp/search_results.rb +7 -1
  104. data/lib/woods/mcp/server.rb +24 -4
  105. data/lib/woods/module_reconciliation.rb +151 -0
  106. data/lib/woods/path_dispatcher.rb +7 -2
  107. data/lib/woods/railtie_support.rb +8 -0
  108. data/lib/woods/rake_helpers.rb +43 -11
  109. data/lib/woods/release.rb +1 -1
  110. data/lib/woods/resilience/index_validator.rb +8 -3
  111. data/lib/woods/resilience/retryable_provider.rb +18 -1
  112. data/lib/woods/resolved_config.rb +68 -8
  113. data/lib/woods/retrieval/context_assembler.rb +3 -3
  114. data/lib/woods/retrieval/lexical_assembler.rb +3 -2
  115. data/lib/woods/retrieval/scope.rb +18 -2
  116. data/lib/woods/retrieval/source_evidence.rb +14 -2
  117. data/lib/woods/source_contributor_validation.rb +78 -0
  118. data/lib/woods/source_contributors.rb +116 -0
  119. data/lib/woods/source_inputs/handoff.rb +37 -0
  120. data/lib/woods/source_inputs/launcher.rb +53 -13
  121. data/lib/woods/source_inputs/manifest.rb +84 -3
  122. data/lib/woods/source_inputs/private_key.rb +44 -12
  123. data/lib/woods/source_inputs/scanner.rb +98 -27
  124. data/lib/woods/source_inputs/scopes.rb +1 -1
  125. data/lib/woods/source_inputs/session.rb +147 -15
  126. data/lib/woods/source_inputs/stable_reader.rb +127 -0
  127. data/lib/woods/source_inputs/status.rb +40 -8
  128. data/lib/woods/source_inputs/verifier.rb +28 -5
  129. data/lib/woods/source_path_encoding.rb +33 -0
  130. data/lib/woods/source_references/cache.rb +284 -0
  131. data/lib/woods/source_references/collector.rb +120 -0
  132. data/lib/woods/source_references/extraction.rb +185 -0
  133. data/lib/woods/source_references/inputs.rb +134 -0
  134. data/lib/woods/source_references/parser_adapter.rb +134 -0
  135. data/lib/woods/source_references/pass.rb +152 -0
  136. data/lib/woods/source_references/prism_adapter.rb +116 -0
  137. data/lib/woods/source_references/registry.rb +178 -0
  138. data/lib/woods/source_references/runtime_lookup.rb +127 -0
  139. data/lib/woods/source_references/value_class.rb +82 -0
  140. data/lib/woods/storage/metadata_store.rb +4 -1
  141. data/lib/woods/storage/qdrant.rb +2 -2
  142. data/lib/woods/unblocked/client.rb +12 -7
  143. data/lib/woods/unblocked/document_builder.rb +4 -1
  144. data/lib/woods/unblocked/exporter.rb +127 -37
  145. data/lib/woods/unblocked/sync_manifest.rb +137 -21
  146. data/lib/woods/unblocked/uri_migration.rb +105 -0
  147. data/lib/woods/util/host_guard.rb +3 -2
  148. data/lib/woods/version.rb +1 -1
  149. data/lib/woods/watch/catch_up.rb +138 -0
  150. data/lib/woods/watch/claim_lease.rb +150 -0
  151. data/lib/woods/watch/cli.rb +26 -2
  152. data/lib/woods/watch/daemon.rb +80 -59
  153. data/lib/woods/watch/installation/options.rb +1 -1
  154. data/lib/woods/watch/installation/receipt.rb +6 -1
  155. data/lib/woods/watch/managed_child.rb +1 -1
  156. data/lib/woods/watch/supervisor.rb +1 -1
  157. data/lib/woods/watch/tree_scan.rb +14 -2
  158. data/plugin/.claude-plugin/plugin.json +1 -1
  159. data/plugin/hooks/adapters/normalize.rb +3 -2
  160. data/plugin/hooks/woods-input-rules.sh +4 -0
  161. data/plugin/hooks/woods-refresh.sh +15 -7
  162. data/plugin/hooks/woods-session-start.sh +60 -3
  163. data/plugin/skills/woods-diagnose/SKILL.md +336 -2
  164. data/plugin/skills/woods-investigate/SKILL.md +11 -0
  165. data/plugin/skills/woods-mcp-config/SKILL.md +86 -0
  166. data/plugin/skills/woods-setup/SKILL.md +56 -1
  167. metadata +34 -5
@@ -115,7 +115,7 @@ Columns:
115
115
  | `include_framework_sources` | Boolean | `true` | user-settable | Extract Rails and gem source code |
116
116
  | `concurrent_extraction` | Boolean | `false` | user-settable | Enable parallel extraction (experimental) |
117
117
  | `vector_store` / `metadata_store` / `graph_store` / `embedding_provider` | Symbol | n/a | preset-derived | Adapter types. Set by presets; override individually to mix stacks. |
118
- | chars-per-token ratio (used by ContextAssembler, TextPreparer, Builder, cost_model) | Float | `4.0` (OpenAI) / `1.5` (Ollama) | computed | Derived from the active embedding provider via `Woods::TokenUtils.chars_per_token_for(...)`. Not directly user-settable; change `embedding_provider` to change the ratio. |
118
+ | chars-per-token ratio (used by ContextAssembler, TextPreparer, Builder, cost_model) | Float | `4.0` (OpenAI) / `1.5` (Ollama) | computed | A sizing estimate derived from the active provider via `Woods::TokenUtils.chars_per_token_for(...)`, not a proof of token count. Complete embedding admission uses the provider-specific bound described below. |
119
119
 
120
120
  ## Embedding options
121
121
 
@@ -136,6 +136,26 @@ config.embedding_options = {
136
136
  }
137
137
  ```
138
138
 
139
+ `dimensions:` explicitly requests an output width; use it to reduce a
140
+ `text-embedding-3-small` or `text-embedding-3-large` vector. The legacy singular
141
+ `dimension:` remains an alias for this explicit request; if both are supplied,
142
+ they must agree. `expected_dimensions:` records an expected output width
143
+ without requesting a reduction. Standalone snapshot restoration uses that
144
+ separate channel. Provider responses must match the expected width before
145
+ Woods accepts them.
146
+
147
+ `text-embedding-ada-002` has a fixed width of 1,536. A matching declared width
148
+ is accepted without sending the API's unsupported `dimensions` parameter;
149
+ other widths are refused before a request.
150
+
151
+ Embedding snapshots store observed `dimension` separately from
152
+ `requested_dimensions`. Standalone readers restore both. For legacy snapshots
153
+ without the latter field, Woods restores a reduction only when the model is
154
+ exactly `text-embedding-3-small` or `text-embedding-3-large` and the recorded
155
+ width is below that model's native width. It does not infer support for
156
+ reductions from arbitrary model names. Store and snapshot width checks still
157
+ apply; changing models or widths requires rebuilding a compatible vector store.
158
+
139
159
  OpenAI embedding batches are sent in slices of at most 36 texts, preserving
140
160
  input order. For inputs within the API's 8,192-token per-text limit, this stays
141
161
  below both the 2,048-input limit and the 300,000-token total request limit.
@@ -160,14 +180,20 @@ config.embedding_options = {
160
180
 
161
181
  The provider reads `model:`, `host:`, and `num_ctx:` from `embedding_options`. `num_ctx` is auto-selected from a per-model registry (`nomic-embed-text` → 2048, `bge-m3` → 8192, `mxbai-embed-large` → 512, `snowflake-arctic-embed` → 512, `snowflake-arctic-embed2` → 8192, `all-minilm` → 512). Unknown models fall back to 2048, matching Ollama's conservative embedding default. Set `num_ctx:` explicitly only when running a model with a known-larger native context that isn't in the registry yet.
162
182
 
183
+ An explicit `dimensions:` option (or its legacy `dimension:` alias) is sent to
184
+ Ollama and requires server/model support. A stored width is restored through
185
+ `expected_dimensions:` instead.
186
+ Discovering a width through a probe never adds `dimensions` to later requests.
187
+
163
188
  **Why `num_ctx` is capped at the native context.** Ollama has an open regression ([ollama/ollama#14186](https://github.com/ollama/ollama/issues/14186)) where `options.num_ctx` does not lift the effective ceiling on `/api/embed` for models whose native context is smaller than the override. Woods advertises the native ceiling so the chunker sizes inputs to what Ollama will actually accept.
164
189
 
165
- **Optional exact tokenization.** Install the [`tokenizers`](https://github.com/ankane/tokenizers-ruby) gem alongside Woods to get BERT WordPiece token counting. Without it, Woods falls back to a chars/token ratio, which under-counts dense Ruby source (CamelCase constants, callback DSLs) and can silently over-pack chunks. Recommended for any Ollama setup.
166
-
167
- ```ruby
168
- # Gemfile (optional)
169
- gem 'tokenizers', '~> 0.5'
170
- ```
190
+ **Input counting (included in Woods 2.1).** Complete inputs include metadata
191
+ prefixes. Known OpenAI embedding models use a conservative byte upper bound;
192
+ Ollama/custom model counts remain estimates. Woods requests `truncate: false`
193
+ from Ollama and refuses/splits oversized inputs without dropping source.
194
+ Installing `tokenizers` no longer implicitly downloads BERT or establishes an
195
+ exact count for an unrelated model. A caller-supplied local tokenizer must match
196
+ the selected model. See the model guide for limitations.
171
197
 
172
198
  See [EMBEDDING_MODELS.md](EMBEDDING_MODELS.md) for the full model comparison and the procedure for adding a new model to the registry.
173
199
 
@@ -188,6 +214,39 @@ Anything responding to `#embed` and `#embed_batch` can be assigned directly, it
188
214
  config.embedding_provider = MyCompany::CustomEmbedder.new(endpoint: internal_url)
189
215
  ```
190
216
 
217
+ Included in Woods 2.1: snapshots capture the effective settings of injected
218
+ Ollama, OpenAI, and Fake instances, including through Woods' retry and embedding
219
+ cache wrappers. An injected instance takes precedence over unused
220
+ `embedding_options`. Ollama retains its model, host, context size, read timeout,
221
+ and vector settings; OpenAI and Fake retain their model and vector settings.
222
+ Capturing constructor settings makes no requests. When a live `provider:` is
223
+ passed to `ResolvedConfig.from_configuration`, its existing dimension-discovery
224
+ behavior remains available. The explicit request width remains separate from
225
+ the observed width.
226
+
227
+ API keys are never stored; standalone OpenAI restoration still requires
228
+ `OPENAI_API_KEY`. Only plain HTTP(S) origins can be persisted as Ollama hosts
229
+ (an optional trailing `/` is allowed). URLs containing user information, a path
230
+ prefix, query, or fragment are omitted and marked `requires_host_provider`.
231
+ Those components may be legitimate routing settings, but can also carry
232
+ credentials; Woods refuses to reconstruct a different endpoint by dropping them.
233
+ The same restriction applies when reading older snapshot endpoints.
234
+
235
+ **Reader compatibility:** snapshots marked `requires_host_provider` need a
236
+ supporting reader. Older schema-1 readers cannot enforce that marker and may
237
+ silently select defaults; do not use them for implicit restoration of these
238
+ snapshots. Ordinary built-in snapshots with non-secret settings remain
239
+ schema-1 compatible.
240
+
241
+ Custom providers and custom wrappers are also marked `requires_host_provider`;
242
+ Woods does not serialize arbitrary client state or infer a built-in from a custom
243
+ class name. Embedding in the configured host continues to work. To serve such a
244
+ snapshot semantically, provide an explicitly configured compatible provider in
245
+ the reader's host initializer. Implicit standalone restoration reports a
246
+ configuration error instead of choosing defaults. Explicit lexical retrieval
247
+ does not restore or call an embedding provider. Record the loaded revision
248
+ before relying on these snapshot improvements.
249
+
191
250
  ## Storage options
192
251
 
193
252
  | Option | Type | Default | Description |
@@ -264,6 +323,15 @@ worktree when overriding it. Existing databases at the old configured output
264
323
  path are not moved or deleted; run `woods:embed` for the selected index after
265
324
  upgrading to populate its default metadata database.
266
325
 
326
+ **Included in Woods 2.1:** embedding with dump-backed vectors reconciles the
327
+ complete SQLite identity inventory, including records without vectors and
328
+ same-name units with distinct types. A full rebuild removes vanished records,
329
+ including when the new corpus is empty. Incremental runs retain the existing
330
+ empty-input and 30% purge guards. Both modes still refuse incomplete extraction
331
+ input before changing stores. Standalone MCP opens the index's SQLite metadata
332
+ before restoring type filters on hydrated vectors, using that same metadata
333
+ store for retrieval. No database migration or embedding format change is required.
334
+
267
335
  Requires the `sqlite3` gem in your host bundle. Rails apps backed by
268
336
  MySQL or PostgreSQL won't have it by default, selecting `:sqlite`
269
337
  without it raises `Woods::ConfigurationError` with install
@@ -326,9 +394,33 @@ overrides the wrapper defaults for `:embeddings` (24 hours) and `:context`
326
394
  (15 minutes). `:memory` accepts `max_entries` (default 500); it ignores
327
395
  `default_ttl` because each wrapper write supplies its domain TTL.
328
396
 
397
+ **Included in Woods 2.1:** retrieval contexts are scoped to one retriever
398
+ instance, including when Redis or Solid Cache is shared by applications or
399
+ worktrees. Restarting the retriever starts a fresh context namespace. Reload
400
+ retires only that instance's namespace, including results still in flight;
401
+ already running requests may finish against the previous corpus. Retired entries
402
+ expire according to their configured TTL or backend eviction; disabling both
403
+ can retain unused entries indefinitely. Embedding-vector caches are separate and survive context invalidation.
404
+
405
+ Embedding entries are scoped to the provider family, endpoint, model, requested
406
+ and declared widths, and relevant input options. Cache lookups never call a
407
+ provider's probing `dimensions` method. Keys contain hashes rather than raw
408
+ endpoint credentials or API keys. Malformed cached vectors and known-width
409
+ mismatches are refused. This configuration key format starts a fresh embedding
410
+ cache after upgrading; old entries expire under their existing TTLs.
411
+
412
+ Custom provider objects may expose a pure, JSON-compatible `cache_identity`
413
+ containing every setting that affects their vectors, plus a non-probing
414
+ `configured_dimensions` reader for width validation. Otherwise each cache
415
+ wrapper has its own namespace; cached vectors are still checked for valid
416
+ numeric content and consistent batch widths. Configuration changes require a
417
+ new provider and wrapper. A remotely replaced model under an unchanged name
418
+ still requires an explicit cache/model-version change and re-embedding.
419
+
329
420
  `Woods::Cache.cache_key` length-prefixes every component, including a single
330
- component, so different argument counts cannot share a response. Existing
331
- multi-component keys used by Woods' wrappers remain unchanged. Custom callers
421
+ component, so different argument counts cannot share a response. This component
422
+ encoding is unchanged and independent of the retriever's context namespace;
423
+ the embedding wrapper uses the new identity above. Custom callers
332
424
  using single-component keys must clear their affected persistent cache domain
333
425
  when upgrading, since older unprefixed entries can alias the new encoding;
334
426
  subsequent calls refill it normally. Namespace clearing still covers both formats.
@@ -496,22 +588,35 @@ there is no full population count to publish. Both caps must be a positive Integ
496
588
 
497
589
  | Option | Type | Default | Description |
498
590
  |--------|------|---------|-------------|
499
- | `session_tracer_enabled` | Boolean | `false` | Enable session tracing middleware |
591
+ | `session_tracer_enabled` | Boolean | `false` | Enable session tracing middleware; set with its store in `config/application.rb` before Railtie initialization |
500
592
  | `session_tracer_allow_production` | Boolean | `false` | Explicitly allow session tracing in `Rails.env.production?`. Without this opt-in, the Railtie warns and leaves the tracer disabled even when `session_tracer_enabled` is true. Review trace contents, retention, and access controls before enabling it. |
501
593
  | `session_store` | Object | `nil` | Store backend: `FileStore`, `RedisStore`, or `SolidCacheStore` |
502
594
  | `session_id_proc` | Proc | `nil` | Custom proc to extract session ID from requests |
503
595
  | `session_exclude_paths` | Array<String> | `[]` | Path patterns to exclude from tracing |
504
596
 
597
+ Configure the complete tracer setup in `config/application.rb`, after Woods is
598
+ required and before Rails initialization. A `config/initializers/woods.rb`
599
+ assignment is too late to mount this middleware; Woods warns and tracing stays
600
+ disabled. Restart the server after changing it.
601
+
505
602
  ```ruby
506
- require 'woods/session_tracer/file_store' # the stores are not autoloaded
603
+ # config/application.rb, after Bundler.require(*Rails.groups)
604
+ require 'woods/session_tracer/file_store'
507
605
 
508
- config.session_tracer_enabled = true
509
- config.session_store = Woods::SessionTracer::FileStore.new(
510
- base_dir: Rails.root.join('tmp/session_traces')
511
- )
512
- config.session_exclude_paths = ['/health', '/metrics', '/assets']
606
+ Woods.configure do |config|
607
+ config.session_tracer_enabled = true
608
+ config.session_store = Woods::SessionTracer::FileStore.new(
609
+ base_dir: File.expand_path('../tmp/session_traces', __dir__)
610
+ )
611
+ config.session_exclude_paths = ['/health', '/metrics', '/assets']
612
+ end
513
613
  ```
514
614
 
615
+ The Rails middleware records traces. Reading them with `session_trace` additionally
616
+ requires a custom Index Server process configured with the same store; the
617
+ packaged Index executable does not load application initializers. See
618
+ [conditional capabilities](MCP_SERVERS.md#conditional-index-capabilities).
619
+
515
620
  ### File session retention
516
621
 
517
622
  `FileStore` accepts `ttl:` in seconds (default `nil`, expiration disabled),
@@ -682,8 +787,8 @@ deployment guide including defense layers.
682
787
  | `console_mcp_enabled` | Boolean | `false` | Master switch. When `false`, stdio exits and the mounted Console middleware passes requests through to Rails. |
683
788
  | `console_mcp_http_enabled` | Boolean | `true` | HTTP transport switch; effective only while the master switch is on. Set `false` for stdio-only use without HTTP token validation or an active HTTP endpoint. Read at request time. |
684
789
  | `console_mcp_token` | String | `ENV['WOODS_CONSOLE_MCP_TOKEN']` or `nil` | Bearer token required on every enabled Console HTTP request. With both Console flags enabled, production boot raises on a missing token; other environments warn and requests fail closed with 401. A configured token shorter than 32 characters raises at boot while HTTP is enabled. Explicit stdio-only configurations skip HTTP token validation. Generate with `SecureRandom.hex(32)`. |
685
- | `console_mcp_allowed_origins` | Array\<String\> | `%w[http://localhost http://127.0.0.1 http://[::1]]` | `OriginGuard` allowlist. Port is stripped before comparison, so `http://localhost` matches any localhost port. Override for tunneled / internal-dashboard access. |
686
- | `console_mcp_path` | String | `/mcp/console` | URL path the Rack middleware responds on. |
790
+ | `console_mcp_allowed_origins` | Array\<String\> | `%w[http://localhost http://127.0.0.1 http://[::1]]` | Shared guard/SDK allowlist (included in Woods 2.1). Exact cross-origin entries include the port; portless entries also permit same-authority traffic. Include the actual browser origin and non-loopback endpoint authority. Restart after changes. |
791
+ | `console_mcp_path` | String | `/mcp/console` | URL path captured when Rack middleware mounts. Set a custom path in `config/application.rb` before Railtie initialization, then restart. |
687
792
  | `console_embedded_read_tools` | Boolean | `false` | Register `console_sql` and `console_query` in supported stdio and Rack modes. |
688
793
  | `console_blocked_tables` | Array\<String\> | `Woods::DEFAULT_CONSOLE_BLOCKED_TABLES` | TableGate denylist (case-insensitive). Bare names match every schema; qualified names (`schema.table`) match exactly. |
689
794
  | `console_redacted_columns` | Array\<String\> | `Woods::DEFAULT_CONSOLE_REDACTED_COLUMNS` | Column names whose values are replaced with `[REDACTED]` in responses, and which are refused as aggregate, scope, find, and order inputs. |
@@ -785,6 +890,11 @@ in its finalized development environment. See [startup and installation](WATCH_D
785
890
  for the generator's explicit modes, portable receipt, update/removal, and the
786
891
  separate supervision status. Raw task settings above remain compatible.
787
892
 
893
+ **Included in Woods 2.1 (#591):** blank/whitespace-only idle timeouts count as
894
+ unset consistently. `woods-watch --recover-claim INDEX --claim-token TOKEN`
895
+ provides explicit recovery only for a matching abandoned managed lifetime lease;
896
+ see [ownership recovery](WATCH_DAEMON.md#recovering-an-abandoned-managed-claim).
897
+
788
898
  ### Opt-in plugin refresh hooks
789
899
 
790
900
  These settings control the plugin shell worker. Check installed
@@ -813,6 +923,11 @@ tasks for manual refreshes; hook transport is not a general shell execution API.
813
923
 
814
924
  ### Extraction rake tasks
815
925
 
926
+ The separate `woods-extract` launcher captures before Rails configuration runs:
927
+ custom `output_dir` applications must supply matching `--output` or `WOODS_OUTPUT`.
928
+ Its implicit default is `tmp/woods`; the Woods 2.1 #591 guard refuses a finalized
929
+ configuration mismatch before publication. See [launcher output selection](SOURCE_FRESHNESS.md#establish-a-fresh-baseline).
930
+
816
931
  | Variable | Default | Purpose |
817
932
  |----------|---------|---------|
818
933
  | `WOODS_OUTPUT` | `Woods.configuration.output_dir` | Overrides the output directory for extraction/watch and embedding tasks without editing the initializer; embedding also places its default SQLite metadata database there. Explicit database options take precedence. |
@@ -209,7 +209,18 @@ Rails/MCP host. If a browser-based client sends an `Origin` header from a
209
209
  different host, include that exact origin too. This allow-list controls both
210
210
  DNS-rebinding Host checks and browser CORS; keep Rails' own `config.hosts`, TLS,
211
211
  and proxy rules aligned with it. Server-to-server clients normally omit
212
- `Origin`, but their request `Host` must still be allowed.
212
+ `Origin`, but a present request `Host` must still be allowed. Supporting security
213
+ revisions deliberately allow configured non-loopback Hosts through the SDK too;
214
+ 2.0.0 could refuse them at that inner layer despite the Woods allowlist. This
215
+ widening is limited to the configured authorities and retains bearer auth. The
216
+ same immutable policy makes preflight acceptance agree with SDK dispatch. List exact browser ports for
217
+ cross-port traffic, including loopback; a portless origin additionally permits
218
+ same-authority traffic, not arbitrary cross-port access. Console defaults remain
219
+ HTTP loopback, while the Index Server defaults include HTTP and HTTPS loopback.
220
+ Default HTTP(S) ports are equivalent to their omitted form. An explicit list
221
+ replaces default browser origins, including loopback origins; add loopback entries
222
+ if those clients are needed. Invalid entries fail at boot naming the entry.
223
+ Restart after changing the list. See [HTTP origin matching](MCP_HTTP_TRANSPORT.md#browser-origins-dns-rebinding-defense).
213
224
 
214
225
  Do not mount `Woods::Console::RackMiddleware` by itself. The Railtie composes
215
226
  `OriginGuard`, `BearerAuth`, and the Console middleware in the supported order.
@@ -329,6 +340,21 @@ user: deploy
329
340
  command: cd /app && bundle exec rake woods:console
330
341
  ```
331
342
 
343
+ **Included in Woods 2.1:** direct mode prefers an existing executable `bin/rake`
344
+ in the application's selected directory, falling back to `bundle exec rake`.
345
+ Relative `directory` values resolve from the launcher's initial working directory.
346
+ Explicit `command` settings win. Docker and SSH modes retain their remote default;
347
+ set `command` explicitly when that application uses another task entry point.
348
+
349
+ **Included in Woods 2.1:** the launcher rejects unknown keys, nested
350
+ `connection:`/`console:` sections, blank or non-string values, and options that
351
+ do not apply to the selected mode. Keep `mode` and `command` at the top level;
352
+ use `directory` for direct mode, `container` for Docker, and `host`/`user` for SSH.
353
+ Configure access controls and redaction in the Rails initializer, not this
354
+ process-launch file. Move legacy nested launch settings to the matching example
355
+ above. An empty file or mapping still selects the default direct command;
356
+ supported `command`/`directory` overrides can omit `mode` for direct execution.
357
+
332
358
  Override config path with environment variable:
333
359
 
334
360
  ```bash
@@ -376,6 +402,13 @@ servers register only executable tools: the 9 Tier 1 tools by default, plus
376
402
  | `console_association_count` | Count associated records for a specific record |
377
403
  | `console_recent` | Recently created/updated records (max 50) |
378
404
 
405
+ **Included in Woods 2.1:** `console_association_count` returns `0` or `1` for
406
+ `belongs_to` and `has_one`, including missing targets. It counts the association's
407
+ relation, applying `scope` to the target model; collection associations retain
408
+ their ordinary count. Target-table and scope-column checks still apply before
409
+ association reads. The tool does not call a custom association getter to count
410
+ an already loaded model object.
411
+
379
412
  ### Tier 2: Domain-aware (9 tools): inventory only, not executable
380
413
 
381
414
  | Tool | Description |
@@ -420,9 +453,21 @@ servers register only executable tools: the 9 Tier 1 tools by default, plus
420
453
 
421
454
  ## Configuration options
422
455
 
423
- Set these in your Rails initializer:
456
+ Set the request-time flags and protection settings below in your Rails initializer.
457
+ A **custom endpoint path** must be set earlier, after Woods is required in
458
+ `config/application.rb`, because the Railtie captures it while mounting middleware:
424
459
 
425
460
  ```ruby
461
+ # config/application.rb, after Bundler.require(*Rails.groups)
462
+ Woods.configure { |config| config.console_mcp_path = '/internal/woods-console' }
463
+ ```
464
+
465
+ Restart after changing the path. Leaving the default `/mcp/console` requires no
466
+ path assignment. Session tracing also needs early configuration; follow
467
+ [session tracer options](CONFIGURATION_REFERENCE.md#session-tracer-options).
468
+
469
+ ```ruby
470
+ # config/initializers/woods.rb
426
471
  Woods.configure do |config|
427
472
  # Master on/off switch for the Console MCP feature (Layer 0). Default: false.
428
473
  # Applies to every transport: stdio, launcher wrapper, and Rack.
@@ -432,9 +477,6 @@ Woods.configure do |config|
432
477
  # after configuring the layers below that match your threat model.
433
478
  config.console_mcp_enabled = true
434
479
 
435
- # URL path for the Rack middleware endpoint. Default: '/mcp/console'.
436
- config.console_mcp_path = '/mcp/console'
437
-
438
480
  # HTTP Origin + Host allow-list. Defaults to loopback only. Non-loopback
439
481
  # Rack deployments must include their public MCP host; browser clients from
440
482
  # another origin need that exact origin listed too.
@@ -596,6 +638,10 @@ Direct, unaliased selection of a redacted column stays allowed: the output heade
596
638
 
597
639
  `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.
598
640
 
641
+ The security correction refuses PostgreSQL Unicode-escaped identifiers in
642
+ `console_sql` before execution. Use ordinary identifiers or standard quoted
643
+ identifiers instead. This restriction does not change the structured query tools.
644
+
599
645
  `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.
600
646
 
601
647
  **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`.
@@ -636,6 +682,9 @@ Redaction is defense-in-depth, prefer not storing plaintext secrets in database
636
682
 
637
683
  ### `console_redacted_key_values`
638
684
 
685
+ See [read policy compatibility](#read-policy-compatibility) for SQL column-list
686
+ restrictions, conservative typed masking, exact key spelling and binary columns.
687
+
639
688
  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.
640
689
 
641
690
  `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`":
@@ -700,7 +749,7 @@ these controls, in order:
700
749
 
701
750
  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.
702
751
  2. `TableGate` refuses any SQL, model, or join that touches a `console_blocked_tables` entry.
703
- 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.
752
+ 3. `SafeContext` wraps every request in a rolled-back transaction with an adapter-dependent 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.
704
753
  4. `CredentialScanner` + column/EAV redaction scrub results.
705
754
 
706
755
  Keep the flag off when the host requires a narrower database capability.
@@ -718,7 +767,7 @@ supported transport (stdio, Docker/SSH launcher, and HTTP).
718
767
  | 1 | Blocked tables | `console_blocked_tables` | Tool dispatch, before executor | Reject any tool call that touches a named table (model, table, or sql arg) |
719
768
  | 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 |
720
769
  | 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 |
721
- | 4 | SqlValidator + SafeContext | built-in | Inside executor | SQL deny-list for `console_sql`; transaction-rollback for every request |
770
+ | 4 | SqlValidator + SafeContext | built-in | Inside executor | SQL validation and function allowlist for `console_sql`; transaction rollback for every request |
722
771
 
723
772
  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.
724
773
 
@@ -754,13 +803,20 @@ boundary remain necessary.
754
803
 
755
804
  ### Statement timeout
756
805
 
757
- Each transaction sets a statement timeout before any query runs. The default is **5000ms** (5 seconds). Timeout enforcement is adapter-specific:
806
+ SafeContext attempts a **5000ms** (5-second) timeout. Support depends on the
807
+ adapter and server; an unsupported setting is skipped and logged when a Rails
808
+ logger is available.
758
809
 
759
810
  | Adapter | Mechanism | Scope |
760
811
  |---------|-----------|-------|
761
- | PostgreSQL | `SET statement_timeout = '5000ms'` | All statement types |
762
- | MySQL | `SET max_execution_time = 5000` (session scope; the prior value is restored after the transaction) | SELECT only (MySQL limitation) |
763
- | Other | Best-effort (skipped gracefully) | n/a |
812
+ | PostgreSQL | `SET LOCAL statement_timeout = '5000ms'` | Transaction-local; discarded on rollback |
813
+ | MySQL | `SET max_execution_time = 5000` | SELECT only; previous session value restored in `ensure` |
814
+ | MariaDB | `SET max_statement_time = 5.0` | Seconds; previous session value restored in `ensure` |
815
+ | SQLite / unrecognized family | No supported per-statement timeout | Do not rely on a query time limit |
816
+
817
+ Rollback remains active when a timeout setting is unsupported. Recognition of a
818
+ MySQL-family adapter alone does not establish that its server supports the
819
+ corresponding timeout variable.
764
820
 
765
821
  ### SQL validation (tier 4 `console_sql`)
766
822
 
@@ -771,7 +827,7 @@ Validation runs **once**, inside the executor, with the dialect of the live adap
771
827
 
772
828
  - **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).
773
829
  - **Rejected prefixes:** `INSERT`, `UPDATE`, `DELETE`, `MERGE`, `DROP`, `ALTER`, `TRUNCATE`, `CREATE`, `GRANT`, `REVOKE`
774
- - **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 all supported normalizations. Every view is scanned under both MySQL executable-comment (`/*!...*/`) semantics, so `#` comments and version-guarded comments cannot split a clause apart.
830
+ - **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. Direct validator calls without a dialect conservatively scan all supported normalizations; packaged `console_sql` refuses unrecognized adapter families. Every view is scanned under both MySQL executable-comment (`/*!...*/`) semantics, so `#` comments and version-guarded comments cannot split a clause apart.
775
831
  - **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.
776
832
  - **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
777
833
 
@@ -843,6 +899,23 @@ fail closed with `Woods::ConfigurationError`.
843
899
 
844
900
  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.
845
901
 
902
+ **Included in Woods 2.1:** if that eager load raises `NameError` (including a
903
+ Zeitwerk naming error), HTTP Console returns a stable `503` and records one
904
+ `console.eager_load.failed` diagnostic. That worker remains unavailable until
905
+ restart; it does not serve a partially populated model registry or repeatedly
906
+ retry loading on incoming requests. The response and diagnostic omit the raw
907
+ application exception. Reproduce it in the same application environment:
908
+
909
+ ```bash
910
+ bundle exec rails runner 'Rails.application.eager_load!'
911
+ ```
912
+
913
+ Fix the application error, then restart the Rails workers. Authentication and
914
+ requests outside the Console mount retain their normal behavior. Other exception
915
+ types retain Rails' usual failure handling. The existing stdio entry point warns
916
+ on eager-load `NameError` and continues; its model list may be incomplete, so fix
917
+ the same application error there too.
918
+
846
919
  ### Timeout errors on large models
847
920
 
848
921
  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:
@@ -902,3 +975,59 @@ These corrections require `2.0.0.beta4` or a reviewed development revision that
902
975
  contains them. Confirm that a patched release is available before selecting it.
903
976
  On affected versions, disable Console where these policies are required; Index MCP
904
977
  can stay enabled because it reads the published code index separately.
978
+
979
+ ## Console corrections included in 2.1
980
+
981
+ Woods 2.1 includes the reviewed Console corrections also released in the
982
+ 2.0.1 and 1.6.4 security patches. For Git/path installations, verify the loaded
983
+ revision and gem path. Plugin updates alone do not update the Console server.
984
+ See the published [security advisory](https://github.com/lost-in-the/woods/security/advisories/GHSA-wxxx-6hqc-qm8g).
985
+
986
+ The patch tightens SQL policy for adapter-specific comment and quoting forms,
987
+ whole-row and multi-source redaction, typed key-value records, and per-mount HTTP
988
+ guards. Supported structured reads remain available. Raw `console_sql` requires
989
+ a recognized PostgreSQL, MySQL-family, or SQLite adapter; compatible subclasses
990
+ are recognized by ancestry. Unsupported raw SQL receives a clear refusal without
991
+ disabling structured tools.
992
+
993
+ Malformed HTTP origin configuration now refuses at boot with the offending entry.
994
+ Fix the allowlist and restart; do not relax authentication or redaction to make a
995
+ refused request succeed. For stdio-only use, keep HTTP disabled.
996
+
997
+ ### Read policy compatibility
998
+
999
+ These rules describe Woods 2.1 and the 2.0.1 security patch. Differences from
1000
+ the 1.6.4 backport are called out below. Verify the installed version and, for
1001
+ Git/path installations, the revision and loaded gem path.
1002
+
1003
+ - **Column alias lists:** when either `console_redacted_columns` or
1004
+ `console_redacted_key_values` is nonempty, `console_sql` refuses relation
1005
+ and CTE column alias lists before execution, including lists on base tables,
1006
+ derived tables, parenthesized `VALUES` sources and table functions. This applies
1007
+ even when the selected names are not protected and independently of function
1008
+ validation. Ordinary relation
1009
+ aliases, CTEs without column lists and allowed scalar functions remain subject
1010
+ to the normal SQL policy. Use explicit, unaliased protected columns or a
1011
+ structured Console tool.
1012
+ - **Conservative typed masking:** EAV type lookup matches the final source-table
1013
+ name case-insensitively and includes every matching registered model, even
1014
+ across schemas. Any matching type can cause masking. This deliberately may
1015
+ mask extra values when table names differ only by case or share that final
1016
+ name; qualifying the table does not narrow that type set.
1017
+ - **Exact sensitive values:** `sensitive_keys` compares the stored and cast key
1018
+ values with exact case after string conversion. Configure their actual raw or
1019
+ cast spelling. `CredentialIndex` also matches credential substrings with exact
1020
+ case; it does not decode hexadecimal binary output such as PostgreSQL `bytea`.
1021
+ Binary cells have no general text-scanning guarantee: non-UTF-8 data can fail
1022
+ JSON normalization before scanning. Put binary secret columns in
1023
+ `console_redacted_columns` so they are masked before serialization.
1024
+ - **Adapter boundary:** raw `console_sql` requires PostgreSQL, MySQL-family
1025
+ (including Mysql2, MariaDB and Trilogy), or SQLite classification. Compatible
1026
+ subclasses are recognized by ancestry. Unknown families receive a validation
1027
+ refusal for raw SQL; structured tools remain available under their normal
1028
+ gates. See [statement timeouts](#statement-timeout) for adapter limits.
1029
+ - **SQL functions and select entries:** 2.x enforces a read-only function
1030
+ allowlist; 1.6.x retains a function denylist. For `console_query`, provide one
1031
+ expression per `select` array entry. 2.x refuses comma-combined entries that
1032
+ 1.6.4 splits before validation. Execution and typed redaction use the same
1033
+ validated projection on all patched lines.
@@ -37,8 +37,8 @@ Woods.configure do |config|
37
37
  model: 'bge-m3',
38
38
  host: 'http://localhost:11434'
39
39
  }
40
- # Everything else, chunker sizing, token counting, num_ctx, is picked
41
- # up automatically from the model name.
40
+ # Model context and initial chunk sizing follow the registry.
41
+ # Local counts are estimates; the server enforces its actual limit.
42
42
  end
43
43
  ```
44
44
 
@@ -64,21 +64,20 @@ override. For `nomic-embed-text` (native 2048) the server rejects inputs above
64
64
  that with a `400 "the input length exceeds the context length"` regardless of
65
65
  `num_ctx`.
66
66
 
67
- Woods works around this two ways:
68
-
69
- 1. **Advertise the native ceiling, not the override.** `Provider::Ollama` keeps
70
- a model → native-context registry (`MODEL_CONTEXT_LENGTHS`) so the chunker
71
- sizes inputs to what Ollama will actually accept. `num_ctx` is still passed
72
- through the request body in case the regression is fixed upstream, but
73
- nothing relies on it.
74
- 2. **Verify client-side with the real tokenizer.** When the
75
- [`tokenizers`](https://github.com/ankane/tokenizers-ruby) gem is installed,
76
- `Embedding::TokenCounter` loads the `bert-base-uncased` WordPiece tokenizer
77
- (the one every BERT-family embedding model is built on) and the chunker
78
- re-verifies every slice. WordPiece fragments CamelCase and `::` separators
79
- differently than character-based estimation suggests, this verification is
80
- what catches the 10–20 % gap between our estimate and Ollama's internal
81
- count.
67
+ **Included in Woods 2.1:** Woods checks complete inputs, including their
68
+ metadata prefixes, and splits source without discarding characters. OpenAI's
69
+ known byte-BPE embedding models use a conservative UTF-8 byte upper bound at
70
+ 8191 tokens; this may create more chunks than an exact tokenizer would.
71
+ Ollama and custom models use explicitly labelled estimates. Ollama requests
72
+ set `truncate: false`, so an actual overflow fails instead of losing source.
73
+ A prefix that cannot fit or one unsplittable source character refuses before
74
+ vector/checkpoint writes; inspect the reported unit, model and limit.
75
+
76
+ Installing `tokenizers` alone no longer downloads or selects BERT for every
77
+ model. Advanced callers can inject a local model-matched tokenizer into
78
+ `Embedding::TokenCounter`; the caller must establish that it matches the server.
79
+ No automatic tokenizer download occurs. These checks do not prove that estimated
80
+ inputs fit an arbitrary tokenizer: the provider remains authoritative.
82
81
 
83
82
  ## Adding a new model to the registry
84
83
 
@@ -112,8 +111,6 @@ If you want Woods to auto-pick `num_ctx` for a model we don't ship support for:
112
111
 
113
112
  - Indexing a real-world Rails app where concern-inlined models or long service
114
113
  objects routinely exceed 2048 tokens.
115
- - You already pay the `tokenizers` gem install cost and want the tighter
116
- coupling between client-side verification and server-side enforcement.
117
114
  - Multilingual content (Arctic Embed 2 is BGE M3 with a stronger non-English
118
115
  story).
119
116