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.
Files changed (108) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +186 -0
  3. data/README.md +20 -8
  4. data/exe/woods-console +51 -6
  5. data/exe/woods-console-mcp +24 -4
  6. data/exe/woods-mcp +30 -7
  7. data/exe/woods-mcp-http +47 -6
  8. data/lib/generators/woods/install_generator.rb +13 -4
  9. data/lib/generators/woods/templates/woods.rb.tt +155 -0
  10. data/lib/tasks/woods.rake +69 -50
  11. data/lib/woods/builder.rb +174 -9
  12. data/lib/woods/cache/cache_middleware.rb +360 -31
  13. data/lib/woods/chunking/semantic_chunker.rb +334 -7
  14. data/lib/woods/console/adapters/job_adapter.rb +10 -4
  15. data/lib/woods/console/audit_logger.rb +76 -4
  16. data/lib/woods/console/bridge.rb +48 -15
  17. data/lib/woods/console/bridge_protocol.rb +44 -0
  18. data/lib/woods/console/confirmation.rb +3 -4
  19. data/lib/woods/console/console_response_renderer.rb +56 -18
  20. data/lib/woods/console/credential_index.rb +201 -0
  21. data/lib/woods/console/credential_scanner.rb +302 -0
  22. data/lib/woods/console/dispatch_pipeline.rb +138 -0
  23. data/lib/woods/console/embedded_executor.rb +682 -35
  24. data/lib/woods/console/eval_guard.rb +319 -0
  25. data/lib/woods/console/model_validator.rb +1 -3
  26. data/lib/woods/console/rack_middleware.rb +185 -29
  27. data/lib/woods/console/redactor.rb +161 -0
  28. data/lib/woods/console/response_context.rb +127 -0
  29. data/lib/woods/console/safe_context.rb +220 -23
  30. data/lib/woods/console/scope_predicate_parser.rb +131 -0
  31. data/lib/woods/console/server.rb +417 -486
  32. data/lib/woods/console/sql_noise_stripper.rb +87 -0
  33. data/lib/woods/console/sql_table_scanner.rb +213 -0
  34. data/lib/woods/console/sql_validator.rb +81 -31
  35. data/lib/woods/console/table_gate.rb +93 -0
  36. data/lib/woods/console/tool_specs.rb +552 -0
  37. data/lib/woods/console/tools/tier1.rb +3 -3
  38. data/lib/woods/console/tools/tier4.rb +7 -1
  39. data/lib/woods/dependency_graph.rb +66 -7
  40. data/lib/woods/embedding/indexer.rb +190 -6
  41. data/lib/woods/embedding/openai.rb +40 -4
  42. data/lib/woods/embedding/provider.rb +104 -8
  43. data/lib/woods/embedding/text_preparer.rb +23 -3
  44. data/lib/woods/embedding/token_counter.rb +133 -0
  45. data/lib/woods/evaluation/baseline_runner.rb +20 -2
  46. data/lib/woods/evaluation/metrics.rb +4 -1
  47. data/lib/woods/extracted_unit.rb +1 -0
  48. data/lib/woods/extractor.rb +7 -1
  49. data/lib/woods/extractors/controller_extractor.rb +6 -0
  50. data/lib/woods/extractors/mailer_extractor.rb +16 -2
  51. data/lib/woods/extractors/model_extractor.rb +6 -1
  52. data/lib/woods/extractors/phlex_extractor.rb +13 -4
  53. data/lib/woods/extractors/rails_source_extractor.rb +2 -0
  54. data/lib/woods/extractors/route_helper_resolver.rb +130 -0
  55. data/lib/woods/extractors/shared_dependency_scanner.rb +130 -2
  56. data/lib/woods/extractors/view_component_extractor.rb +12 -1
  57. data/lib/woods/extractors/view_engines/base.rb +141 -0
  58. data/lib/woods/extractors/view_engines/erb.rb +145 -0
  59. data/lib/woods/extractors/view_template_extractor.rb +92 -133
  60. data/lib/woods/flow_assembler.rb +23 -15
  61. data/lib/woods/flow_precomputer.rb +21 -2
  62. data/lib/woods/graph_analyzer.rb +210 -0
  63. data/lib/woods/index_artifact.rb +173 -0
  64. data/lib/woods/mcp/bearer_auth.rb +45 -0
  65. data/lib/woods/mcp/bootstrap_state.rb +94 -0
  66. data/lib/woods/mcp/bootstrapper.rb +337 -16
  67. data/lib/woods/mcp/config_resolver.rb +288 -0
  68. data/lib/woods/mcp/errors.rb +134 -0
  69. data/lib/woods/mcp/index_reader.rb +265 -30
  70. data/lib/woods/mcp/origin_guard.rb +132 -0
  71. data/lib/woods/mcp/provider_probe.rb +166 -0
  72. data/lib/woods/mcp/renderers/claude_renderer.rb +6 -0
  73. data/lib/woods/mcp/renderers/markdown_renderer.rb +100 -3
  74. data/lib/woods/mcp/renderers/plain_renderer.rb +16 -2
  75. data/lib/woods/mcp/server.rb +771 -137
  76. data/lib/woods/model_name_cache.rb +78 -2
  77. data/lib/woods/notion/client.rb +25 -2
  78. data/lib/woods/notion/mappers/model_mapper.rb +36 -2
  79. data/lib/woods/railtie.rb +55 -15
  80. data/lib/woods/resilience/circuit_breaker.rb +9 -2
  81. data/lib/woods/resilience/retryable_provider.rb +40 -3
  82. data/lib/woods/resolved_config.rb +299 -0
  83. data/lib/woods/retrieval/context_assembler.rb +112 -5
  84. data/lib/woods/retrieval/query_classifier.rb +1 -1
  85. data/lib/woods/retrieval/ranker.rb +55 -6
  86. data/lib/woods/retrieval/search_executor.rb +42 -13
  87. data/lib/woods/retriever.rb +330 -24
  88. data/lib/woods/session_tracer/middleware.rb +35 -1
  89. data/lib/woods/storage/graph_store.rb +39 -0
  90. data/lib/woods/storage/inapplicable_backend.rb +14 -0
  91. data/lib/woods/storage/metadata_store.rb +129 -1
  92. data/lib/woods/storage/pgvector.rb +70 -8
  93. data/lib/woods/storage/qdrant.rb +196 -5
  94. data/lib/woods/storage/snapshotter/metadata.rb +172 -0
  95. data/lib/woods/storage/snapshotter/vector.rb +238 -0
  96. data/lib/woods/storage/snapshotter.rb +24 -0
  97. data/lib/woods/storage/vector_store.rb +184 -35
  98. data/lib/woods/tasks.rb +85 -0
  99. data/lib/woods/temporal/snapshot_store.rb +49 -1
  100. data/lib/woods/token_utils.rb +44 -5
  101. data/lib/woods/unblocked/client.rb +163 -0
  102. data/lib/woods/unblocked/document_builder.rb +326 -0
  103. data/lib/woods/unblocked/exporter.rb +201 -0
  104. data/lib/woods/unblocked/rate_limiter.rb +94 -0
  105. data/lib/woods/util/host_guard.rb +61 -0
  106. data/lib/woods/version.rb +1 -1
  107. data/lib/woods.rb +130 -6
  108. 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