woods 1.1.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 +186 -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 +69 -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 +210 -0
- 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 +100 -3
- data/lib/woods/mcp/renderers/plain_renderer.rb +16 -2
- data/lib/woods/mcp/server.rb +771 -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 +163 -0
- data/lib/woods/unblocked/document_builder.rb +326 -0
- data/lib/woods/unblocked/exporter.rb +201 -0
- data/lib/woods/unblocked/rate_limiter.rb +94 -0
- data/lib/woods/util/host_guard.rb +61 -0
- data/lib/woods/version.rb +1 -1
- data/lib/woods.rb +130 -6
- metadata +73 -4
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'json'
|
|
4
|
+
require_relative 'response_context'
|
|
5
|
+
|
|
6
|
+
module Woods
|
|
7
|
+
module Console
|
|
8
|
+
# Per-tool dispatch flow for the Console MCP server.
|
|
9
|
+
#
|
|
10
|
+
# Encapsulates everything that happens between MCP's `define_tool` block
|
|
11
|
+
# firing and the `MCP::Tool::Response` returning to the client:
|
|
12
|
+
#
|
|
13
|
+
# 1. Coerce integer-typed args from strings. MCP clients sometimes send
|
|
14
|
+
# `"10"` for an `integer` property — we normalize before dispatch.
|
|
15
|
+
# 2. Run the Layer 1 table gate ({ResponseContext#enforce!}).
|
|
16
|
+
# 3. Translate the tool args into a bridge/executor request via the
|
|
17
|
+
# handler proc supplied in the {ToolSpec}.
|
|
18
|
+
# 4. Send the request through the connection manager / embedded executor.
|
|
19
|
+
# 5. Apply Layer 3 column + EAV redaction to the result
|
|
20
|
+
# ({ResponseContext#redact}).
|
|
21
|
+
# 6. Apply Layer 2 credential scanning to the result and/or error text
|
|
22
|
+
# ({ResponseContext#scan}), logging hit counts when any fire.
|
|
23
|
+
# 7. Render the result via the optional renderer or JSON, wrap in a
|
|
24
|
+
# response object.
|
|
25
|
+
#
|
|
26
|
+
# Previously this flow lived inline in `Server.register` with four
|
|
27
|
+
# `method(:...)` captures closing over module-level methods. Pulling it
|
|
28
|
+
# into a first-class object collapses that wiring, removes the need for
|
|
29
|
+
# the captures, and makes the pipeline directly testable without going
|
|
30
|
+
# through the full server build path.
|
|
31
|
+
class DispatchPipeline
|
|
32
|
+
# @param tool_name [String]
|
|
33
|
+
# @param handler [#call] Maps a Hash of tool args to a bridge request Hash.
|
|
34
|
+
# @param integer_keys [Array<Symbol>] Property keys declared as `integer`
|
|
35
|
+
# in the tool schema.
|
|
36
|
+
# @param conn_mgr [#send_request] ConnectionManager or EmbeddedExecutor.
|
|
37
|
+
# @param ctx [ResponseContext] Bundles the three response-safety layers.
|
|
38
|
+
# Defaults to the Null context so tests can stub minimally.
|
|
39
|
+
# @param renderer [#render_default, nil] Optional response renderer.
|
|
40
|
+
# @param logger [#warn, nil] Structured logger used for credential-scan
|
|
41
|
+
# and table-gate telemetry. Failures are swallowed so observability
|
|
42
|
+
# issues never break a tool response.
|
|
43
|
+
def initialize(tool_name:, handler:, integer_keys:, conn_mgr:, # rubocop:disable Metrics/ParameterLists
|
|
44
|
+
ctx: NullResponseContext.instance, renderer: nil, logger: nil)
|
|
45
|
+
@tool_name = tool_name
|
|
46
|
+
@handler = handler
|
|
47
|
+
@integer_keys = integer_keys
|
|
48
|
+
@conn_mgr = conn_mgr
|
|
49
|
+
@ctx = ctx
|
|
50
|
+
@renderer = renderer
|
|
51
|
+
@logger = logger
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Run the full dispatch pipeline for a single tool call.
|
|
55
|
+
#
|
|
56
|
+
# @param args [Hash] Tool arguments (symbol keys from MCP).
|
|
57
|
+
# @return [MCP::Tool::Response]
|
|
58
|
+
def call(args)
|
|
59
|
+
coerce_integer_args!(args)
|
|
60
|
+
@ctx.enforce!(args)
|
|
61
|
+
request = @handler.call(args).transform_keys(&:to_s)
|
|
62
|
+
send_to_bridge(request)
|
|
63
|
+
rescue TableGateError => e
|
|
64
|
+
log_table_gate_rejection(args, e)
|
|
65
|
+
error_response(e.message)
|
|
66
|
+
rescue SqlValidationError, ForbiddenExpressionError => e
|
|
67
|
+
error_response(e.message)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
private
|
|
71
|
+
|
|
72
|
+
def coerce_integer_args!(args)
|
|
73
|
+
@integer_keys.each { |k| args[k] = args[k].to_i if args[k].is_a?(String) }
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def send_to_bridge(request)
|
|
77
|
+
response = @conn_mgr.send_request(request)
|
|
78
|
+
return error_from_response(response, request) unless response['ok']
|
|
79
|
+
|
|
80
|
+
result = @ctx.redact(response['result'])
|
|
81
|
+
result = scan_for_credentials(result, request)
|
|
82
|
+
text = @renderer ? @renderer.render_default(result) : JSON.pretty_generate(result)
|
|
83
|
+
success_response(text)
|
|
84
|
+
rescue ConnectionError => e
|
|
85
|
+
scanned = scan_for_credentials("Connection error: #{e.message}", request)
|
|
86
|
+
error_response(scanned)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def error_from_response(response, request)
|
|
90
|
+
error_text = "#{response['error_type']}: #{response['error']}"
|
|
91
|
+
error_text = scan_for_credentials(error_text, request)
|
|
92
|
+
error_response(error_text)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def scan_for_credentials(result, request)
|
|
96
|
+
scanned, counts = @ctx.scan(result)
|
|
97
|
+
log_credential_hits(request, counts) unless counts.empty?
|
|
98
|
+
scanned
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def log_credential_hits(request, counts)
|
|
102
|
+
return unless @logger
|
|
103
|
+
|
|
104
|
+
@logger.warn(
|
|
105
|
+
'console.credential_scan.hits',
|
|
106
|
+
tool: request['tool'],
|
|
107
|
+
counts: counts.transform_keys(&:to_s),
|
|
108
|
+
total: counts.values.sum
|
|
109
|
+
)
|
|
110
|
+
rescue StandardError
|
|
111
|
+
nil
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def log_table_gate_rejection(args, error)
|
|
115
|
+
return unless @logger
|
|
116
|
+
|
|
117
|
+
@logger.warn(
|
|
118
|
+
'console.table_gate.rejected',
|
|
119
|
+
tool: @tool_name,
|
|
120
|
+
model: args[:model],
|
|
121
|
+
table: args[:table],
|
|
122
|
+
has_sql: args[:sql] ? true : false,
|
|
123
|
+
message: error.message
|
|
124
|
+
)
|
|
125
|
+
rescue StandardError
|
|
126
|
+
nil
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def success_response(text)
|
|
130
|
+
::MCP::Tool::Response.new([{ type: 'text', text: text }])
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def error_response(text)
|
|
134
|
+
::MCP::Tool::Response.new([{ type: 'text', text: text }], error: true)
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
end
|