woods 1.2.0 → 1.3.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +169 -0
- data/README.md +20 -8
- data/exe/woods-console +51 -6
- data/exe/woods-console-mcp +24 -4
- data/exe/woods-mcp +30 -7
- data/exe/woods-mcp-http +47 -6
- data/lib/generators/woods/install_generator.rb +13 -4
- data/lib/generators/woods/templates/woods.rb.tt +155 -0
- data/lib/tasks/woods.rake +15 -50
- data/lib/woods/builder.rb +174 -9
- data/lib/woods/cache/cache_middleware.rb +360 -31
- data/lib/woods/chunking/semantic_chunker.rb +334 -7
- data/lib/woods/console/adapters/job_adapter.rb +10 -4
- data/lib/woods/console/audit_logger.rb +76 -4
- data/lib/woods/console/bridge.rb +48 -15
- data/lib/woods/console/bridge_protocol.rb +44 -0
- data/lib/woods/console/confirmation.rb +3 -4
- data/lib/woods/console/console_response_renderer.rb +56 -18
- data/lib/woods/console/credential_index.rb +201 -0
- data/lib/woods/console/credential_scanner.rb +302 -0
- data/lib/woods/console/dispatch_pipeline.rb +138 -0
- data/lib/woods/console/embedded_executor.rb +682 -35
- data/lib/woods/console/eval_guard.rb +319 -0
- data/lib/woods/console/model_validator.rb +1 -3
- data/lib/woods/console/rack_middleware.rb +185 -29
- data/lib/woods/console/redactor.rb +161 -0
- data/lib/woods/console/response_context.rb +127 -0
- data/lib/woods/console/safe_context.rb +220 -23
- data/lib/woods/console/scope_predicate_parser.rb +131 -0
- data/lib/woods/console/server.rb +417 -486
- data/lib/woods/console/sql_noise_stripper.rb +87 -0
- data/lib/woods/console/sql_table_scanner.rb +213 -0
- data/lib/woods/console/sql_validator.rb +81 -31
- data/lib/woods/console/table_gate.rb +93 -0
- data/lib/woods/console/tool_specs.rb +552 -0
- data/lib/woods/console/tools/tier1.rb +3 -3
- data/lib/woods/console/tools/tier4.rb +7 -1
- data/lib/woods/dependency_graph.rb +66 -7
- data/lib/woods/embedding/indexer.rb +190 -6
- data/lib/woods/embedding/openai.rb +40 -4
- data/lib/woods/embedding/provider.rb +104 -8
- data/lib/woods/embedding/text_preparer.rb +23 -3
- data/lib/woods/embedding/token_counter.rb +133 -0
- data/lib/woods/evaluation/baseline_runner.rb +20 -2
- data/lib/woods/evaluation/metrics.rb +4 -1
- data/lib/woods/extracted_unit.rb +1 -0
- data/lib/woods/extractor.rb +7 -1
- data/lib/woods/extractors/controller_extractor.rb +6 -0
- data/lib/woods/extractors/mailer_extractor.rb +16 -2
- data/lib/woods/extractors/model_extractor.rb +6 -1
- data/lib/woods/extractors/phlex_extractor.rb +13 -4
- data/lib/woods/extractors/rails_source_extractor.rb +2 -0
- data/lib/woods/extractors/route_helper_resolver.rb +130 -0
- data/lib/woods/extractors/shared_dependency_scanner.rb +130 -2
- data/lib/woods/extractors/view_component_extractor.rb +12 -1
- data/lib/woods/extractors/view_engines/base.rb +141 -0
- data/lib/woods/extractors/view_engines/erb.rb +145 -0
- data/lib/woods/extractors/view_template_extractor.rb +92 -133
- data/lib/woods/flow_assembler.rb +23 -15
- data/lib/woods/flow_precomputer.rb +21 -2
- data/lib/woods/graph_analyzer.rb +3 -4
- data/lib/woods/index_artifact.rb +173 -0
- data/lib/woods/mcp/bearer_auth.rb +45 -0
- data/lib/woods/mcp/bootstrap_state.rb +94 -0
- data/lib/woods/mcp/bootstrapper.rb +337 -16
- data/lib/woods/mcp/config_resolver.rb +288 -0
- data/lib/woods/mcp/errors.rb +134 -0
- data/lib/woods/mcp/index_reader.rb +265 -30
- data/lib/woods/mcp/origin_guard.rb +132 -0
- data/lib/woods/mcp/provider_probe.rb +166 -0
- data/lib/woods/mcp/renderers/claude_renderer.rb +6 -0
- data/lib/woods/mcp/renderers/markdown_renderer.rb +39 -3
- data/lib/woods/mcp/renderers/plain_renderer.rb +16 -2
- data/lib/woods/mcp/server.rb +737 -137
- data/lib/woods/model_name_cache.rb +78 -2
- data/lib/woods/notion/client.rb +25 -2
- data/lib/woods/notion/mappers/model_mapper.rb +36 -2
- data/lib/woods/railtie.rb +55 -15
- data/lib/woods/resilience/circuit_breaker.rb +9 -2
- data/lib/woods/resilience/retryable_provider.rb +40 -3
- data/lib/woods/resolved_config.rb +299 -0
- data/lib/woods/retrieval/context_assembler.rb +112 -5
- data/lib/woods/retrieval/query_classifier.rb +1 -1
- data/lib/woods/retrieval/ranker.rb +55 -6
- data/lib/woods/retrieval/search_executor.rb +42 -13
- data/lib/woods/retriever.rb +330 -24
- data/lib/woods/session_tracer/middleware.rb +35 -1
- data/lib/woods/storage/graph_store.rb +39 -0
- data/lib/woods/storage/inapplicable_backend.rb +14 -0
- data/lib/woods/storage/metadata_store.rb +129 -1
- data/lib/woods/storage/pgvector.rb +70 -8
- data/lib/woods/storage/qdrant.rb +196 -5
- data/lib/woods/storage/snapshotter/metadata.rb +172 -0
- data/lib/woods/storage/snapshotter/vector.rb +238 -0
- data/lib/woods/storage/snapshotter.rb +24 -0
- data/lib/woods/storage/vector_store.rb +184 -35
- data/lib/woods/tasks.rb +85 -0
- data/lib/woods/temporal/snapshot_store.rb +49 -1
- data/lib/woods/token_utils.rb +44 -5
- data/lib/woods/unblocked/client.rb +1 -1
- data/lib/woods/unblocked/document_builder.rb +35 -10
- data/lib/woods/unblocked/exporter.rb +1 -1
- data/lib/woods/util/host_guard.rb +61 -0
- data/lib/woods/version.rb +1 -1
- data/lib/woods.rb +126 -6
- metadata +69 -4
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'errors'
|
|
4
|
+
require_relative '../resolved_config'
|
|
5
|
+
|
|
6
|
+
module Woods
|
|
7
|
+
module MCP
|
|
8
|
+
# Resolves the embedding configuration that the MCP server should use.
|
|
9
|
+
#
|
|
10
|
+
# Reads +woods.json+ (the resolved config snapshot written at embed time),
|
|
11
|
+
# applies environment-variable overrides, validates, and returns either a
|
|
12
|
+
# populated {Woods::Configuration} or raises a typed {BootstrapError}
|
|
13
|
+
# subclass that the caller can present to an operator.
|
|
14
|
+
#
|
|
15
|
+
# This class is extracted from {Bootstrapper} to isolate the
|
|
16
|
+
# config-resolution concern from network probing and store construction.
|
|
17
|
+
# {Bootstrapper} delegates to {.resolve} as its first step.
|
|
18
|
+
#
|
|
19
|
+
# Resolution order (highest priority first):
|
|
20
|
+
#
|
|
21
|
+
# 1. Host Rails initializer (Woods.configuration already has embedding_provider).
|
|
22
|
+
# When +woods.json+ is also present the stored config is loaded and asserted
|
|
23
|
+
# compatible via {ResolvedConfig#assert_compatible!}.
|
|
24
|
+
# 2. +woods.json+ snapshot alone (MCP server running without a host initializer).
|
|
25
|
+
# The stored config is used to populate +config+ in place.
|
|
26
|
+
# 3. Environment-variable auto-detect (deprecated, opt-in via
|
|
27
|
+
# +WOODS_ALLOW_AUTODETECT=1+). Mutates +config+ to set provider and stores.
|
|
28
|
+
# 4. If none of the above applies, raises {MissingArtifact}.
|
|
29
|
+
#
|
|
30
|
+
# @example Typical Bootstrapper usage
|
|
31
|
+
# config, source = ConfigResolver.resolve(Woods.configuration, artifact: artifact)
|
|
32
|
+
# # config.embedding_provider is now guaranteed to be non-nil (or :none was returned)
|
|
33
|
+
#
|
|
34
|
+
module ConfigResolver
|
|
35
|
+
# Resolve and validate the embedding configuration.
|
|
36
|
+
#
|
|
37
|
+
# When +woods.json+ is present in the artifact directory it is parsed into
|
|
38
|
+
# a {ResolvedConfig} and, if the host already has a provider configured,
|
|
39
|
+
# validated for compatibility (dimension + provider class/model must agree).
|
|
40
|
+
# If the host has no provider configured the stored config is applied to
|
|
41
|
+
# +config+ so {Builder} can construct the correct provider.
|
|
42
|
+
#
|
|
43
|
+
# When +woods.json+ is absent and the host already has a provider
|
|
44
|
+
# configured, the host config is trusted and used as-is.
|
|
45
|
+
#
|
|
46
|
+
# When +woods.json+ is absent and the host has no provider configured:
|
|
47
|
+
# - If +WOODS_ALLOW_AUTODETECT+ is +1+, falls through to env-var
|
|
48
|
+
# auto-detect (deprecated, emits a structured warning).
|
|
49
|
+
# - Otherwise raises {MissingArtifact}.
|
|
50
|
+
#
|
|
51
|
+
# @param config [Woods::Configuration] the live host configuration object.
|
|
52
|
+
# May be mutated in the auto-detect path.
|
|
53
|
+
# @param artifact [Woods::IndexArtifact, nil] the on-disk artifact wrapper.
|
|
54
|
+
# @param env [Hash] environment variable source (default +ENV+, overridable
|
|
55
|
+
# in specs without stubbing the global ENV).
|
|
56
|
+
# @param ollama_probe [#call, nil] callable used to check Ollama reachability
|
|
57
|
+
# in the deprecated auto-detect path. Defaults to {.ollama_reachable?} on
|
|
58
|
+
# this module. Callers may pass a different probe to facilitate testing
|
|
59
|
+
# without touching global state.
|
|
60
|
+
# @return [Array(Woods::Configuration, Symbol)] tuple of +[config, source]+
|
|
61
|
+
# where +source+ is one of +:snapshot+, +:host_config+, +:autodetect+,
|
|
62
|
+
# or +:none+. The config is the same object passed in, possibly mutated.
|
|
63
|
+
# @raise [Woods::MCP::MissingArtifact] when +woods.json+ is absent, the
|
|
64
|
+
# host has no provider configured, and +WOODS_ALLOW_AUTODETECT+ is unset.
|
|
65
|
+
# @raise [Woods::MCP::UnsupportedArtifact] when +woods.json+ has an
|
|
66
|
+
# unsupported +schema_version+.
|
|
67
|
+
# @raise [Woods::MCP::DimensionMismatch] when stored and live provider
|
|
68
|
+
# dimensions disagree.
|
|
69
|
+
# @raise [Woods::MCP::ConfigMismatch] when stored and live provider
|
|
70
|
+
# class/model disagree.
|
|
71
|
+
def self.resolve(config, artifact:, env: ENV, ollama_probe: nil)
|
|
72
|
+
stored = read_stored_config(artifact)
|
|
73
|
+
|
|
74
|
+
if stored
|
|
75
|
+
[apply_stored_config(config, stored, artifact: artifact, env: env), :snapshot]
|
|
76
|
+
elsif config.embedding_provider
|
|
77
|
+
# Host initializer configured a provider; no woods.json to validate
|
|
78
|
+
# against. Trust the host config and proceed.
|
|
79
|
+
[config, :host_config]
|
|
80
|
+
else
|
|
81
|
+
resolve_without_artifact(config, artifact: artifact, env: env, ollama_probe: ollama_probe)
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# Read and parse +woods.json+ if it exists.
|
|
86
|
+
#
|
|
87
|
+
# @param artifact [Woods::IndexArtifact, nil]
|
|
88
|
+
# @return [Woods::ResolvedConfig, nil]
|
|
89
|
+
# @raise [Woods::MCP::UnsupportedArtifact] if the file has an unsupported
|
|
90
|
+
# schema version.
|
|
91
|
+
def self.read_stored_config(artifact)
|
|
92
|
+
return nil unless artifact&.config_path&.exist?
|
|
93
|
+
|
|
94
|
+
raw = artifact.read_config
|
|
95
|
+
return nil unless raw
|
|
96
|
+
|
|
97
|
+
ResolvedConfig.from_hash(raw)
|
|
98
|
+
end
|
|
99
|
+
private_class_method :read_stored_config
|
|
100
|
+
|
|
101
|
+
# Reconcile the live host config against a stored {ResolvedConfig}.
|
|
102
|
+
#
|
|
103
|
+
# When the host has an +embedding_provider+ configured, assert that it
|
|
104
|
+
# matches the stored config. When the host has no provider configured,
|
|
105
|
+
# populate +config+ from the stored values so {Builder} can construct
|
|
106
|
+
# the correct provider.
|
|
107
|
+
#
|
|
108
|
+
# @param config [Woods::Configuration]
|
|
109
|
+
# @param stored [Woods::ResolvedConfig]
|
|
110
|
+
# @param artifact [Woods::IndexArtifact]
|
|
111
|
+
# @return [Woods::Configuration]
|
|
112
|
+
def self.apply_stored_config(config, stored, artifact:, env: ENV)
|
|
113
|
+
if config.embedding_provider
|
|
114
|
+
live = live_resolved_config(config)
|
|
115
|
+
live.assert_compatible!(stored)
|
|
116
|
+
config
|
|
117
|
+
else
|
|
118
|
+
populate_from_stored(config, stored, artifact: artifact, env: env)
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
private_class_method :apply_stored_config
|
|
122
|
+
|
|
123
|
+
# Build a ResolvedConfig from the live host config, probing the
|
|
124
|
+
# provider's dimension when possible. Tolerant of probe failures
|
|
125
|
+
# (unreachable Ollama): falls back to the declared-only path so
|
|
126
|
+
# {ResolvedConfig#assert_compatible!} surfaces a typed
|
|
127
|
+
# {DimensionMismatch} instead of a raw +Errno::ECONNREFUSED+
|
|
128
|
+
# escaping the caller's {BootstrapError} rescue.
|
|
129
|
+
def self.live_resolved_config(config)
|
|
130
|
+
require_relative '../builder'
|
|
131
|
+
provider = Woods::Builder.new(config).build_embedding_provider
|
|
132
|
+
ResolvedConfig.from_configuration(config, provider: provider)
|
|
133
|
+
rescue StandardError
|
|
134
|
+
ResolvedConfig.from_configuration(config)
|
|
135
|
+
end
|
|
136
|
+
private_class_method :live_resolved_config
|
|
137
|
+
|
|
138
|
+
# Populate a blank {Woods::Configuration} from a stored {ResolvedConfig}.
|
|
139
|
+
#
|
|
140
|
+
# Maps the serialised provider class name back to the +:ollama+ /
|
|
141
|
+
# +:openai+ symbol that {Builder} expects, and restores store types.
|
|
142
|
+
# Applied when an MCP server starts without a host Rails initializer.
|
|
143
|
+
#
|
|
144
|
+
# @param config [Woods::Configuration]
|
|
145
|
+
# @param stored [Woods::ResolvedConfig]
|
|
146
|
+
# @param artifact [Woods::IndexArtifact]
|
|
147
|
+
# @return [Woods::Configuration]
|
|
148
|
+
def self.populate_from_stored(config, stored, artifact:, env: ENV)
|
|
149
|
+
config.vector_store = stored.stores[:vector_store] || :in_memory
|
|
150
|
+
config.metadata_store = stored.stores[:metadata_store] || :in_memory
|
|
151
|
+
config.graph_store = stored.stores[:graph_store] || :in_memory
|
|
152
|
+
provider_sym = provider_symbol(stored.embedding_provider[:class])
|
|
153
|
+
config.embedding_provider = provider_sym
|
|
154
|
+
|
|
155
|
+
opts = {}
|
|
156
|
+
opts[:model] = stored.embedding_provider[:model] if stored.embedding_provider[:model]
|
|
157
|
+
opts[:host] = stored.embedding_provider[:host] if stored.embedding_provider[:host]
|
|
158
|
+
opts[:num_ctx] = stored.embedding_provider[:num_ctx] if stored.embedding_provider[:num_ctx]
|
|
159
|
+
opts[:read_timeout] = stored.embedding_provider[:read_timeout] if stored.embedding_provider[:read_timeout]
|
|
160
|
+
opts[:dimension] = stored.embedding_provider[:dimension] if stored.embedding_provider[:dimension]&.positive?
|
|
161
|
+
|
|
162
|
+
# OpenAI needs an api_key to construct. `woods.json` deliberately
|
|
163
|
+
# never stores credentials; pull from env here so the standalone
|
|
164
|
+
# MCP server can boot against an OpenAI-embedded index. Raise a
|
|
165
|
+
# typed error if the env var is missing — previously the generic
|
|
166
|
+
# ArgumentError from OpenAI.new(**opts) wasn't a BootstrapError
|
|
167
|
+
# and the top-level rescue in exe/woods-mcp wouldn't catch it.
|
|
168
|
+
if provider_sym == :openai
|
|
169
|
+
api_key = env.fetch('OPENAI_API_KEY', nil)
|
|
170
|
+
if api_key.nil? || api_key.empty?
|
|
171
|
+
raise Woods::MCP::MissingCredential.new(
|
|
172
|
+
'woods.json says the index was embedded with OpenAI but OPENAI_API_KEY is unset. ' \
|
|
173
|
+
'Export the key before starting the MCP server, or re-embed with a different provider.',
|
|
174
|
+
details: { provider: 'openai', missing_env_var: 'OPENAI_API_KEY' }
|
|
175
|
+
)
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
opts[:api_key] = api_key
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
config.embedding_options = opts unless opts.empty?
|
|
182
|
+
|
|
183
|
+
warn "[woods-mcp] config_source: loaded from woods.json (#{artifact.config_path})"
|
|
184
|
+
config
|
|
185
|
+
end
|
|
186
|
+
private_class_method :populate_from_stored
|
|
187
|
+
|
|
188
|
+
# Convert a fully-qualified provider class name back to the symbol
|
|
189
|
+
# that {Builder} understands.
|
|
190
|
+
#
|
|
191
|
+
# @param class_name [String]
|
|
192
|
+
# @return [Symbol, String] symbol for known providers, raw string otherwise
|
|
193
|
+
def self.provider_symbol(class_name)
|
|
194
|
+
case class_name.to_s
|
|
195
|
+
when /Ollama/ then :ollama
|
|
196
|
+
when /OpenAI/ then :openai
|
|
197
|
+
else class_name.to_s
|
|
198
|
+
end
|
|
199
|
+
end
|
|
200
|
+
private_class_method :provider_symbol
|
|
201
|
+
|
|
202
|
+
# Handle the no-artifact, no-host-provider case.
|
|
203
|
+
#
|
|
204
|
+
# Raises {MissingArtifact} unless +WOODS_ALLOW_AUTODETECT=1+ opts in to
|
|
205
|
+
# the deprecated env-var auto-detect path.
|
|
206
|
+
#
|
|
207
|
+
# @param config [Woods::Configuration]
|
|
208
|
+
# @param artifact [Woods::IndexArtifact, nil]
|
|
209
|
+
# @param env [Hash]
|
|
210
|
+
# @param ollama_probe [#call, nil]
|
|
211
|
+
# @return [Woods::Configuration]
|
|
212
|
+
# @raise [Woods::MCP::MissingArtifact]
|
|
213
|
+
def self.resolve_without_artifact(config, artifact:, env:, ollama_probe:)
|
|
214
|
+
if env['WOODS_ALLOW_AUTODETECT'] != '1'
|
|
215
|
+
raise MissingArtifact.new(
|
|
216
|
+
'No woods.json found and WOODS_ALLOW_AUTODETECT is unset. ' \
|
|
217
|
+
'Run `bundle exec rake woods:extract` in your host app, or set ' \
|
|
218
|
+
'WOODS_ALLOW_AUTODETECT=1 to probe env vars (deprecated).',
|
|
219
|
+
details: { output_dir: artifact&.output_dir&.to_s }
|
|
220
|
+
)
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
warn '[woods-mcp] deprecated_autodetect: falling back to env-var auto-detect (no woods.json found)'
|
|
224
|
+
[autodetect_from_env(config, env: env, ollama_probe: ollama_probe), :autodetect]
|
|
225
|
+
end
|
|
226
|
+
private_class_method :resolve_without_artifact
|
|
227
|
+
|
|
228
|
+
# Probe environment variables for provider credentials and configure
|
|
229
|
+
# +config+ accordingly.
|
|
230
|
+
#
|
|
231
|
+
# Only reachable when +WOODS_ALLOW_AUTODETECT=1+ and no +woods.json+
|
|
232
|
+
# is present. Mutates +config+ to set provider and stores when a
|
|
233
|
+
# credential or reachable Ollama instance is found.
|
|
234
|
+
#
|
|
235
|
+
# @param config [Woods::Configuration]
|
|
236
|
+
# @param env [Hash]
|
|
237
|
+
# @param ollama_probe [#call, nil] callable for Ollama reachability check;
|
|
238
|
+
# defaults to {.ollama_reachable?} on this module when nil.
|
|
239
|
+
# @return [Woods::Configuration]
|
|
240
|
+
def self.autodetect_from_env(config, env:, ollama_probe:)
|
|
241
|
+
probe = ollama_probe || method(:ollama_reachable?)
|
|
242
|
+
openai_key = env.fetch('OPENAI_API_KEY', nil)
|
|
243
|
+
if openai_key
|
|
244
|
+
config.vector_store = :in_memory
|
|
245
|
+
config.metadata_store = :in_memory
|
|
246
|
+
config.graph_store = :in_memory
|
|
247
|
+
config.embedding_provider = :openai
|
|
248
|
+
config.embedding_options = { api_key: openai_key }
|
|
249
|
+
elsif probe.call
|
|
250
|
+
config.vector_store = :in_memory
|
|
251
|
+
config.metadata_store = :in_memory
|
|
252
|
+
config.graph_store = :in_memory
|
|
253
|
+
config.embedding_provider = :ollama
|
|
254
|
+
config.embedding_options = {
|
|
255
|
+
host: env.fetch('OLLAMA_BASE_URL', 'http://localhost:11434'),
|
|
256
|
+
model: env.fetch('OLLAMA_EMBED_MODEL', 'nomic-embed-text')
|
|
257
|
+
}
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
config
|
|
261
|
+
end
|
|
262
|
+
private_class_method :autodetect_from_env
|
|
263
|
+
|
|
264
|
+
# Check whether Ollama is reachable at the configured base URL.
|
|
265
|
+
#
|
|
266
|
+
# Used as the default probe in the deprecated auto-detect path. Callers
|
|
267
|
+
# may inject a different probe via +ollama_probe:+ kwarg.
|
|
268
|
+
#
|
|
269
|
+
# @return [Boolean]
|
|
270
|
+
def self.ollama_reachable?
|
|
271
|
+
require 'net/http'
|
|
272
|
+
require 'uri'
|
|
273
|
+
|
|
274
|
+
base_url = ENV.fetch('OLLAMA_BASE_URL', 'http://localhost:11434')
|
|
275
|
+
uri = URI.parse(base_url)
|
|
276
|
+
Net::HTTP.start(uri.host, uri.port,
|
|
277
|
+
open_timeout: 0.5, read_timeout: 0.5,
|
|
278
|
+
use_ssl: uri.scheme == 'https') do |http|
|
|
279
|
+
response = http.get('/api/tags')
|
|
280
|
+
!response.is_a?(Net::HTTPServerError)
|
|
281
|
+
end
|
|
282
|
+
rescue StandardError
|
|
283
|
+
false
|
|
284
|
+
end
|
|
285
|
+
private_class_method :ollama_reachable?
|
|
286
|
+
end
|
|
287
|
+
end
|
|
288
|
+
end
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../storage/inapplicable_backend'
|
|
4
|
+
|
|
5
|
+
module Woods
|
|
6
|
+
module MCP
|
|
7
|
+
# Base class for all bootstrap-time failures in the MCP server.
|
|
8
|
+
#
|
|
9
|
+
# Inherits directly from {Woods::Error} — a sibling of {Woods::ConfigurationError},
|
|
10
|
+
# not a child. Artifact/runtime state errors grouped here do not describe
|
|
11
|
+
# declared-configuration problems, so grouping under ConfigurationError would
|
|
12
|
+
# mislead host apps that rescue it.
|
|
13
|
+
#
|
|
14
|
+
# Each subclass carries a +details+ hash with structured context for operator
|
|
15
|
+
# messages. The +exe/woods-mcp+ top-level rescue formats this into a one-line
|
|
16
|
+
# hint.
|
|
17
|
+
#
|
|
18
|
+
# @example Rescue in an MCP entry point
|
|
19
|
+
# rescue Woods::MCP::BootstrapError => e
|
|
20
|
+
# warn "woods-mcp: #{e.class}: #{e.message}"
|
|
21
|
+
# exit 2
|
|
22
|
+
class BootstrapError < Woods::Error
|
|
23
|
+
# @return [Hash] Structured context for operator diagnostics
|
|
24
|
+
attr_reader :details
|
|
25
|
+
|
|
26
|
+
# @param message [String]
|
|
27
|
+
# @param details [Hash] Structured context (artifact paths, versions, etc.)
|
|
28
|
+
def initialize(message = nil, details: {})
|
|
29
|
+
super(message)
|
|
30
|
+
@details = details
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Raised when a required credential (e.g. OpenAI API key) is absent and
|
|
35
|
+
# cannot be resolved from the config snapshot or environment.
|
|
36
|
+
#
|
|
37
|
+
# @example
|
|
38
|
+
# raise Woods::MCP::MissingCredential.new(
|
|
39
|
+
# "OPENAI_API_KEY is required for OpenAI provider",
|
|
40
|
+
# details: { credential: "OPENAI_API_KEY", provider: "openai" }
|
|
41
|
+
# )
|
|
42
|
+
class MissingCredential < BootstrapError; end
|
|
43
|
+
|
|
44
|
+
# Raised when a stored config snapshot contradicts the active host config
|
|
45
|
+
# in a way that cannot be reconciled (e.g. provider class mismatch).
|
|
46
|
+
#
|
|
47
|
+
# @example
|
|
48
|
+
# raise Woods::MCP::ConfigMismatch.new(
|
|
49
|
+
# "Stored config uses Ollama but host config specifies OpenAI",
|
|
50
|
+
# details: { stored: "Ollama", host: "OpenAI" }
|
|
51
|
+
# )
|
|
52
|
+
class ConfigMismatch < BootstrapError; end
|
|
53
|
+
|
|
54
|
+
# Raised when the dimension recorded in the config snapshot or vector dump
|
|
55
|
+
# does not match the dimension reported by the active embedding provider.
|
|
56
|
+
#
|
|
57
|
+
# @example
|
|
58
|
+
# raise Woods::MCP::DimensionMismatch.new(
|
|
59
|
+
# "Provider dimension 1536 does not match stored dimension 768",
|
|
60
|
+
# details: { expected: 768, actual: 1536 }
|
|
61
|
+
# )
|
|
62
|
+
class DimensionMismatch < BootstrapError; end
|
|
63
|
+
|
|
64
|
+
# Raised when the +schema_version+ in +woods.json+ or a dump file is
|
|
65
|
+
# newer than the maximum version this gem release supports, or when the
|
|
66
|
+
# artifact header is corrupted and cannot be parsed.
|
|
67
|
+
#
|
|
68
|
+
# @example
|
|
69
|
+
# raise Woods::MCP::UnsupportedArtifact.new(
|
|
70
|
+
# "woods.json schema_version 3 > supported max 1",
|
|
71
|
+
# details: { artifact_version: 3, max_supported: 1, path: config_path.to_s }
|
|
72
|
+
# )
|
|
73
|
+
class UnsupportedArtifact < BootstrapError; end
|
|
74
|
+
|
|
75
|
+
# Raised when +woods.json+ is absent from +output_dir+ and the env flag
|
|
76
|
+
# +WOODS_ALLOW_AUTODETECT+ is not set. Hosts that have never run an embed
|
|
77
|
+
# see a clear failure message rather than silent degradation.
|
|
78
|
+
#
|
|
79
|
+
# @example
|
|
80
|
+
# raise Woods::MCP::MissingArtifact.new(
|
|
81
|
+
# "No woods.json found in /app/tmp/woods. Run `rake woods:embed` first.",
|
|
82
|
+
# details: { output_dir: "/app/tmp/woods" }
|
|
83
|
+
# )
|
|
84
|
+
class MissingArtifact < BootstrapError; end
|
|
85
|
+
|
|
86
|
+
# Raised when the configured embedding provider cannot be reached at boot.
|
|
87
|
+
# This is a **recoverable** sibling of {BootstrapError} — it is deliberately
|
|
88
|
+
# outside the BootstrapError hierarchy because the MCP server starts degraded
|
|
89
|
+
# and retries on first query rather than refusing to start.
|
|
90
|
+
#
|
|
91
|
+
# {Bootstrapper} catches this internally; nothing upstream should rescue it.
|
|
92
|
+
#
|
|
93
|
+
# @example
|
|
94
|
+
# raise Woods::MCP::ProviderUnreachable.new(
|
|
95
|
+
# "http://host.docker.internal:11434 refused connection",
|
|
96
|
+
# details: { url: "http://host.docker.internal:11434", reason: "connection refused" }
|
|
97
|
+
# )
|
|
98
|
+
class ProviderUnreachable < Woods::Error
|
|
99
|
+
# @return [String] URL that was probed
|
|
100
|
+
attr_reader :url
|
|
101
|
+
# @return [String] Machine-readable failure reason (e.g. "connection_refused",
|
|
102
|
+
# "timeout", "http_500", "unauthorized", "dns_failure")
|
|
103
|
+
attr_reader :reason
|
|
104
|
+
# @return [Hash] Structured context forwarded to operators for diagnosis
|
|
105
|
+
attr_reader :details
|
|
106
|
+
|
|
107
|
+
# Supports two call styles:
|
|
108
|
+
#
|
|
109
|
+
# Woods::MCP::ProviderUnreachable.new(url: "http://...", reason: "timeout")
|
|
110
|
+
# Woods::MCP::ProviderUnreachable.new("message", details: { ... })
|
|
111
|
+
#
|
|
112
|
+
# The kwarg form is preferred — probes have url/reason in hand and the
|
|
113
|
+
# message is derived. The positional form exists for callers that
|
|
114
|
+
# already have a formatted message.
|
|
115
|
+
def initialize(message = nil, url: nil, reason: nil, details: {})
|
|
116
|
+
@url = url || details[:url] || details['url']
|
|
117
|
+
@reason = reason || details[:reason] || details['reason']
|
|
118
|
+
@details = details.dup
|
|
119
|
+
@details[:url] = @url if @url && !@details.key?(:url) && !@details.key?('url')
|
|
120
|
+
@details[:reason] = @reason if @reason && !@details.key?(:reason) && !@details.key?('reason')
|
|
121
|
+
super(message || default_message)
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
private
|
|
125
|
+
|
|
126
|
+
def default_message
|
|
127
|
+
return "#{@url}: #{@reason}" if @url && @reason
|
|
128
|
+
return "provider unreachable (#{@reason})" if @reason
|
|
129
|
+
|
|
130
|
+
'provider unreachable'
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
end
|