woods 1.6.1 → 1.6.2
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 +19 -0
- data/CONTRIBUTING.md +49 -0
- data/README.md +6 -0
- data/exe/woods-mcp-http +5 -3
- data/lib/woods/console/credential_index.rb +20 -2
- data/lib/woods/console/credential_scanner.rb +14 -14
- data/lib/woods/console/credential_scanner_registry.rb +36 -0
- data/lib/woods/console/encrypted_credential_snapshot.rb +16 -0
- data/lib/woods/console/rack_middleware.rb +4 -1
- data/lib/woods/console/server.rb +18 -16
- data/lib/woods/console/tool_specs.rb +1 -2
- data/lib/woods/console/tools/tier1.rb +1 -2
- data/lib/woods/console/tools/tier4.rb +1 -2
- data/lib/woods/mcp/http_transport_options.rb +24 -0
- data/lib/woods/mcp/server.rb +2 -9
- data/lib/woods/mcp/version_aware_tool_dispatch.rb +9 -3
- data/lib/woods/retrieval/query_classifier.rb +1 -2
- data/lib/woods/session_tracer/middleware.rb +1 -2
- data/lib/woods/session_tracer/session_flow_assembler.rb +3 -6
- data/lib/woods/session_tracer/session_flow_document.rb +4 -8
- data/lib/woods/version.rb +1 -1
- metadata +42 -7
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 135a4869f2fdd230990dc723a96d62f19447e51295768392d25bd0125dd4e25a
|
|
4
|
+
data.tar.gz: bdce75a6b653bd5da5f3a7f4e7c9180020235f5a232daa5e58a4d439824e9a34
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 7dbe8d5460b1775c4f785954c88539fb29a76ad2878ee58e5e8ca11616787c87a058b9e9a774867f3cff4afe0c1b8ade137fd657f5249995d594e2ce2d7dfd8e
|
|
7
|
+
data.tar.gz: 99db15fe718972172398fcf1574b33e26e94869beeb0d90c95e7adcb6b41b4a53ad3b3c4fe34067ba01df7e485dec61e73cda5b739dda89dab665f9c6bfb3282
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,25 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [1.6.2] - 2026-09-18
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
|
|
14
|
+
- Refresh encrypted Rails credentials from disk when rebuilding the Console credential index, update every live scanner, and preserve the last valid index if refresh fails. Each response uses one consistent index snapshot.
|
|
15
|
+
Keep credential scanner refresh snapshots limited to live weakly referenced
|
|
16
|
+
scanners on older Ruby versions, avoiding unsafe receiver access after garbage
|
|
17
|
+
collection while preserving updates to every live Console server.
|
|
18
|
+
Keep JSON on `>= 2.19.9, < 3` so supported Rails encoders can serialize metadata after a fresh bundle resolution; JSON 3 removes their `quirks_mode` option. Bound MessagePack below 2 and Railties below 9, retaining their existing minimum versions and the strict package-build gate.
|
|
19
|
+
- Preserve version-aware missing-tool guidance with MCP 0.23 by installing its dispatch override before the SDK captures request handlers.
|
|
20
|
+
|
|
21
|
+
### Build
|
|
22
|
+
|
|
23
|
+
- Add a no-publish, one-off 1.6.2 maintenance preparation flow and installed-package CI. Disable the legacy automatic publisher; publication requires the trusted main maintenance profile and its reviewed candidate SHA.
|
|
24
|
+
|
|
25
|
+
### Security
|
|
26
|
+
|
|
27
|
+
- Require MCP 0.23.0 or newer within the 0.x line for upstream transport security fixes; run `bundle update woods mcp` when upgrading. Ruby 3.0 remains supported. The newer SDK removes the old json-schema/addressable dependency chain. Existing Index and Console HTTP origin settings now reach the SDK Host/Origin guards; cross-origin browser access requires the actual configured port.
|
|
28
|
+
|
|
10
29
|
## [1.6.1] - 2026-07-22
|
|
11
30
|
|
|
12
31
|
### Fixed
|
data/CONTRIBUTING.md
CHANGED
|
@@ -125,3 +125,52 @@ Woods uses runtime introspection, not static parsing. If your feature requires a
|
|
|
125
125
|
## License
|
|
126
126
|
|
|
127
127
|
By contributing, you agree that your contributions will be licensed under the MIT License.
|
|
128
|
+
|
|
129
|
+
## Maintenance release
|
|
130
|
+
|
|
131
|
+
This tree has a one-off, no-publish preparation adapter for the reviewed 1.6.2
|
|
132
|
+
security patch while `main` carries the newer 2.0 prerelease line. It does not
|
|
133
|
+
establish a permanent stable branch or authorize another maintenance version.
|
|
134
|
+
The approved target is `release/1.6.2`, based on immutable `v1.6.1`. Create that
|
|
135
|
+
remote target only after the trusted main maintenance policy is reviewed.
|
|
136
|
+
|
|
137
|
+
From a clean checkout, the only supported transitions are:
|
|
138
|
+
|
|
139
|
+
```sh
|
|
140
|
+
bin/rake "release:reopen[1.6.2.alpha]"
|
|
141
|
+
# Review and commit the generated development-state diff.
|
|
142
|
+
bin/rake "release:prepare[1.6.2]"
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
The tasks never commit, tag, push, dispatch or publish. Never edit VERSION or the
|
|
146
|
+
README maintenance `release-state` banner manually. The first reopen creates that
|
|
147
|
+
banner; prepare updates it and folds classified Unreleased notes and optional
|
|
148
|
+
`changelog/<type>_<slug>.md` entries into a dated release heading. Entry files are
|
|
149
|
+
nonempty Markdown without headings; supported types include `fixed`, `security`,
|
|
150
|
+
`build`, and `documentation`. Invalid transitions, dirty trees, malformed notes,
|
|
151
|
+
and missing/duplicate/unknown fences refuse before writes. This legacy profile
|
|
152
|
+
has no v2 migration guide or v2 surface inventory requirement.
|
|
153
|
+
|
|
154
|
+
Validate the full suite, lint, booted extraction, real Console credential rotation,
|
|
155
|
+
and installed maintenance package tests. CI builds one gem plus its SHA-256
|
|
156
|
+
sidecar into `woods-release-<commit SHA>` and tests that artifact on Ruby 3.0/Rails
|
|
157
|
+
6.0 with exactly MCP 0.23.0 and Ruby 4.0/Rails 8.1 with the latest compatible 0.x SDK.
|
|
158
|
+
MCP >=0.23.0 is required for upstream transport security fixes; update Woods and
|
|
159
|
+
MCP together (`bundle update woods mcp`). Ruby 3.0 remains supported.
|
|
160
|
+
The optional package specs require `WOODS_GEM_PATH` and
|
|
161
|
+
run standalone, without the repository's `spec_helper` or implementation path:
|
|
162
|
+
|
|
163
|
+
```sh
|
|
164
|
+
ruby -rrubygems -e 'load Gem.bin_path("rspec-core", "rspec")' -- \
|
|
165
|
+
--options /dev/null spec/integration/maintenance_packaged_gem_spec.rb
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
Preparation is not publication; check RubyGems before describing 1.6.2 as released.
|
|
169
|
+
The legacy automatic tag publisher is disabled, and Bundler's `release`,
|
|
170
|
+
`release:rubygem_push`, and `release:source_control_push` tasks abort. Only a
|
|
171
|
+
maintainer may later tag the reviewed merge commit and dispatch the trusted
|
|
172
|
+
**main** workflow. That workflow must explicitly allow the exact tag, protected
|
|
173
|
+
maintenance branch, immutable 1.6.1 base, reviewed final candidate SHA, required
|
|
174
|
+
CI jobs, and immutable artifact. An unpinned candidate remains blocked. First merge a reviewed trusted-main profile update pinning `approved_sha` to that
|
|
175
|
+
exact maintenance merge SHA; only then may the maintainer tag it. The
|
|
176
|
+
workflow publishes the already tested gem bytes; never rebuild or publish locally.
|
data/README.md
CHANGED
|
@@ -4,6 +4,12 @@
|
|
|
4
4
|
|
|
5
5
|
# Woods
|
|
6
6
|
|
|
7
|
+
<!-- release-state:maintenance-banner -->
|
|
8
|
+
> **Woods 1.6.2: prepared maintenance candidate.** This tree remains on the 1.6 maintenance line.
|
|
9
|
+
> Preparation does not establish publication. Check [RubyGems versions](https://rubygems.org/gems/woods/versions)
|
|
10
|
+
> before selecting an install version. See [maintenance release policy](CONTRIBUTING.md#maintenance-release).
|
|
11
|
+
<!-- release-state:end -->
|
|
12
|
+
|
|
7
13
|
**Your AI coding assistant is guessing about your Rails app. Woods gives it the real answers.**
|
|
8
14
|
|
|
9
15
|
Rails hides enormous amounts of behavior behind conventions, concerns, and runtime magic. When you ask an AI assistant "what callbacks fire when a User saves?" or "what routes map to this controller?", it guesses from training data — and gets it wrong. Woods runs *inside* your Rails app, extracts what's actually happening at runtime, and serves that context directly to your AI tools via [MCP](https://modelcontextprotocol.io/).
|
data/exe/woods-mcp-http
CHANGED
|
@@ -28,6 +28,7 @@ require_relative '../lib/woods/mcp/server'
|
|
|
28
28
|
require_relative '../lib/woods/mcp/bootstrapper'
|
|
29
29
|
require_relative '../lib/woods/mcp/bearer_auth'
|
|
30
30
|
require_relative '../lib/woods/mcp/origin_guard'
|
|
31
|
+
require_relative '../lib/woods/mcp/http_transport_options'
|
|
31
32
|
require_relative '../lib/woods/embedding/text_preparer'
|
|
32
33
|
require_relative '../lib/woods/embedding/indexer'
|
|
33
34
|
|
|
@@ -63,10 +64,11 @@ server = Woods::MCP::Server.build(
|
|
|
63
64
|
index_dir: index_dir, retriever: retriever, snapshot_store: snapshot_store,
|
|
64
65
|
bootstrap_state: bootstrap_state, retriever_reloader: retriever_reloader
|
|
65
66
|
)
|
|
66
|
-
transport = MCP::Server::Transports::StreamableHTTPTransport.new(server)
|
|
67
|
-
server.transport = transport
|
|
68
|
-
|
|
69
67
|
allowed_origins = ENV.fetch('WOODS_MCP_HTTP_ALLOWED_ORIGINS', '').split(',').map(&:strip).reject(&:empty?)
|
|
68
|
+
transport = MCP::Server::Transports::StreamableHTTPTransport.new(
|
|
69
|
+
server, **Woods::MCP::HttpTransportOptions.for(allowed_origins)
|
|
70
|
+
)
|
|
71
|
+
server.transport = transport
|
|
70
72
|
|
|
71
73
|
inner = proc { |env| transport.handle_request(Rack::Request.new(env)) }
|
|
72
74
|
app = token ? Woods::MCP::BearerAuth.new(inner, token: token) : inner
|
|
@@ -68,6 +68,7 @@ module Woods
|
|
|
68
68
|
MISSING_KEY_ERRORS = %w[
|
|
69
69
|
ActiveSupport::EncryptedConfiguration::MissingKeyError
|
|
70
70
|
ActiveSupport::EncryptedFile::MissingKeyError
|
|
71
|
+
ActiveSupport::EncryptedFile::MissingContentError
|
|
71
72
|
ActiveSupport::MessageEncryptor::InvalidMessage
|
|
72
73
|
].freeze
|
|
73
74
|
|
|
@@ -90,13 +91,19 @@ module Woods
|
|
|
90
91
|
# @return [CredentialIndex] populated index, or an empty index when
|
|
91
92
|
# the credentials store can't be opened.
|
|
92
93
|
def build(rails_app:)
|
|
93
|
-
|
|
94
|
+
refresh(rails_app: rails_app)
|
|
94
95
|
rescue StandardError => e
|
|
95
96
|
raise unless missing_key_error?(e)
|
|
96
97
|
|
|
97
98
|
new(secrets: [])
|
|
98
99
|
end
|
|
99
100
|
|
|
101
|
+
# Strict refresh: construct the replacement completely before a caller
|
|
102
|
+
# installs it. Unlike boot-time .build, errors must preserve the old index.
|
|
103
|
+
def refresh(rails_app:)
|
|
104
|
+
new(secrets: collect_secrets(rails_app))
|
|
105
|
+
end
|
|
106
|
+
|
|
100
107
|
# Emit a structured log warning when any of the given credentials
|
|
101
108
|
# files has a modification time newer than `process_start`. This
|
|
102
109
|
# catches the common "rotated credentials but forgot to restart"
|
|
@@ -136,12 +143,23 @@ module Woods
|
|
|
136
143
|
private
|
|
137
144
|
|
|
138
145
|
def collect_secrets(rails_app)
|
|
139
|
-
config = rails_app.credentials.config
|
|
146
|
+
config = fresh_credentials(rails_app.credentials).config
|
|
140
147
|
collected = []
|
|
141
148
|
walk(config, collected)
|
|
142
149
|
collected
|
|
143
150
|
end
|
|
144
151
|
|
|
152
|
+
def fresh_credentials(credentials)
|
|
153
|
+
return credentials unless defined?(ActiveSupport::EncryptedConfiguration) &&
|
|
154
|
+
credentials.is_a?(ActiveSupport::EncryptedConfiguration)
|
|
155
|
+
|
|
156
|
+
require_relative 'encrypted_credential_snapshot'
|
|
157
|
+
EncryptedCredentialSnapshot.new(
|
|
158
|
+
config_path: credentials.content_path, key_path: credentials.key_path,
|
|
159
|
+
env_key: credentials.env_key, raise_if_missing_key: true
|
|
160
|
+
)
|
|
161
|
+
end
|
|
162
|
+
|
|
145
163
|
def walk(node, collected)
|
|
146
164
|
case node
|
|
147
165
|
when Hash then node.each_value { |v| walk(v, collected) }
|
|
@@ -155,17 +155,17 @@ module Woods
|
|
|
155
155
|
# for patterns that fired — callers should treat a missing key as zero.
|
|
156
156
|
def scan(value)
|
|
157
157
|
counts = {}
|
|
158
|
-
scanned = walk(value, counts)
|
|
158
|
+
scanned = walk(value, counts, @secret_index)
|
|
159
159
|
[scanned, counts]
|
|
160
160
|
end
|
|
161
161
|
|
|
162
162
|
private
|
|
163
163
|
|
|
164
|
-
def walk(value, counts)
|
|
164
|
+
def walk(value, counts, index)
|
|
165
165
|
case value
|
|
166
|
-
when String then scan_string(value, counts)
|
|
167
|
-
when Hash then walk_hash(value, counts)
|
|
168
|
-
when Array then value.map { |item| walk(item, counts) }
|
|
166
|
+
when String then scan_string(value, counts, index)
|
|
167
|
+
when Hash then walk_hash(value, counts, index)
|
|
168
|
+
when Array then value.map { |item| walk(item, counts, index) }
|
|
169
169
|
else value
|
|
170
170
|
end
|
|
171
171
|
end
|
|
@@ -177,19 +177,19 @@ module Woods
|
|
|
177
177
|
# credential-shaped string is emitted as a Symbol after redaction
|
|
178
178
|
# (e.g. `:"[REDACTED]"`). String keys stay Strings. Non-String,
|
|
179
179
|
# non-Symbol keys (Integer, etc.) pass through unchanged.
|
|
180
|
-
def walk_hash(hash, counts)
|
|
180
|
+
def walk_hash(hash, counts, index)
|
|
181
181
|
hash.each_with_object({}) do |(key, val), out|
|
|
182
182
|
scanned_key = case key
|
|
183
|
-
when String then scan_string(key, counts)
|
|
184
|
-
when Symbol then scan_string(key.to_s, counts).to_sym
|
|
183
|
+
when String then scan_string(key, counts, index)
|
|
184
|
+
when Symbol then scan_string(key.to_s, counts, index).to_sym
|
|
185
185
|
else key
|
|
186
186
|
end
|
|
187
|
-
out[scanned_key] = walk(val, counts)
|
|
187
|
+
out[scanned_key] = walk(val, counts, index)
|
|
188
188
|
end
|
|
189
189
|
end
|
|
190
190
|
|
|
191
|
-
def scan_string(str, counts)
|
|
192
|
-
result = redact_indexed_secrets(str, counts)
|
|
191
|
+
def scan_string(str, counts, index)
|
|
192
|
+
result = redact_indexed_secrets(str, counts, index)
|
|
193
193
|
result = scan_encoded_forms(result, counts)
|
|
194
194
|
@active_patterns.inject(result) do |acc, (name, pattern)|
|
|
195
195
|
acc.gsub(pattern) do
|
|
@@ -290,10 +290,10 @@ module Woods
|
|
|
290
290
|
printable.to_f / str.length >= PRINTABLE_RATIO_THRESHOLD
|
|
291
291
|
end
|
|
292
292
|
|
|
293
|
-
def redact_indexed_secrets(str, counts)
|
|
294
|
-
return str unless
|
|
293
|
+
def redact_indexed_secrets(str, counts, index)
|
|
294
|
+
return str unless index&.match?(str)
|
|
295
295
|
|
|
296
|
-
redacted =
|
|
296
|
+
redacted = index.redact(str)
|
|
297
297
|
counts[INDEX_HIT] = (counts[INDEX_HIT] || 0) + redacted.scan(CredentialIndex::REDACTED).size
|
|
298
298
|
redacted
|
|
299
299
|
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Woods
|
|
4
|
+
module Console
|
|
5
|
+
# Coordinates scanner construction and rotation without retaining servers
|
|
6
|
+
# after their transports release them. Requests keep their own index snapshot.
|
|
7
|
+
class CredentialScannerRegistry
|
|
8
|
+
def initialize
|
|
9
|
+
@scanners = ObjectSpace::WeakMap.new
|
|
10
|
+
@mutex = Mutex.new
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def register
|
|
14
|
+
@mutex.synchronize do
|
|
15
|
+
scanner = yield
|
|
16
|
+
# Older Ruby WeakMap implementations validate the value's liveness
|
|
17
|
+
# when enumerating. Keep the scanner in both weak positions so an
|
|
18
|
+
# immortal sentinel cannot make an abandoned scanner look live.
|
|
19
|
+
@scanners[scanner] = scanner
|
|
20
|
+
scanner
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def rebuild
|
|
25
|
+
@mutex.synchronize do
|
|
26
|
+
scanners = @scanners.values
|
|
27
|
+
return nil if scanners.empty?
|
|
28
|
+
|
|
29
|
+
index = yield
|
|
30
|
+
scanners.each { |scanner| scanner.replace_index!(index) }
|
|
31
|
+
index
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'active_support/encrypted_configuration'
|
|
4
|
+
|
|
5
|
+
module Woods
|
|
6
|
+
module Console
|
|
7
|
+
# A private, uncached Rails configuration for one credential-index build.
|
|
8
|
+
# Unlike Rails' permissive configuration reader, a missing encrypted file
|
|
9
|
+
# must raise during an explicit refresh rather than erase the last index.
|
|
10
|
+
class EncryptedCredentialSnapshot < ActiveSupport::EncryptedConfiguration
|
|
11
|
+
def read
|
|
12
|
+
ActiveSupport::EncryptedFile.instance_method(:read).bind(self).call
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require 'json'
|
|
4
|
+
require 'woods/mcp/http_transport_options'
|
|
4
5
|
require 'woods/observability/structured_logger'
|
|
5
6
|
|
|
6
7
|
module Woods
|
|
@@ -131,7 +132,9 @@ module Woods
|
|
|
131
132
|
Rails.application.eager_load!
|
|
132
133
|
|
|
133
134
|
server = build_embedded_server
|
|
134
|
-
@transport = ::MCP::Server::Transports::StreamableHTTPTransport.new(
|
|
135
|
+
@transport = ::MCP::Server::Transports::StreamableHTTPTransport.new(
|
|
136
|
+
server, **Woods::MCP::HttpTransportOptions.for(Woods.configuration.console_mcp_allowed_origins)
|
|
137
|
+
)
|
|
135
138
|
server.transport = @transport
|
|
136
139
|
@transport
|
|
137
140
|
end
|
data/lib/woods/console/server.rb
CHANGED
|
@@ -13,6 +13,7 @@ require_relative 'audit_logger'
|
|
|
13
13
|
require_relative 'confirmation'
|
|
14
14
|
require_relative 'console_response_renderer'
|
|
15
15
|
require_relative 'credential_scanner'
|
|
16
|
+
require_relative 'credential_scanner_registry'
|
|
16
17
|
require_relative 'eval_guard'
|
|
17
18
|
require_relative 'table_gate'
|
|
18
19
|
require_relative 'redactor'
|
|
@@ -34,10 +35,14 @@ module Woods
|
|
|
34
35
|
# transport.open
|
|
35
36
|
#
|
|
36
37
|
module Server # rubocop:disable Metrics/ModuleLength
|
|
38
|
+
SCANNERS = CredentialScannerRegistry.new
|
|
39
|
+
private_constant :SCANNERS
|
|
40
|
+
|
|
37
41
|
class << self # rubocop:disable Metrics/ClassLength
|
|
38
42
|
# Rebuild the boot-time credential index from fresh Rails credentials
|
|
39
|
-
# and hot-swap it into
|
|
43
|
+
# and hot-swap it into every live scanner without restarting the process.
|
|
40
44
|
#
|
|
45
|
+
# Refresh failures propagate without replacing any live index.
|
|
41
46
|
# Host rotation jobs should call this immediately after `rails credentials:edit`
|
|
42
47
|
# changes are deployed. The swap is atomic on MRI (GVL) — in-flight scans see
|
|
43
48
|
# either the old or the new index, never a partial one.
|
|
@@ -57,14 +62,11 @@ module Woods
|
|
|
57
62
|
# the rebuild was skipped.
|
|
58
63
|
def rebuild_credential_index(rails_app: nil)
|
|
59
64
|
return nil unless credential_defense_enabled?
|
|
60
|
-
return nil unless @active_scanner
|
|
61
65
|
|
|
62
66
|
target_app = rails_app || default_rails_app
|
|
63
67
|
return nil unless target_app
|
|
64
68
|
|
|
65
|
-
|
|
66
|
-
@active_scanner.replace_index!(new_index)
|
|
67
|
-
new_index
|
|
69
|
+
SCANNERS.rebuild { CredentialIndex.refresh(rails_app: target_app) }
|
|
68
70
|
end
|
|
69
71
|
|
|
70
72
|
# True when Woods is configured and credential defense is on.
|
|
@@ -399,21 +401,21 @@ module Woods
|
|
|
399
401
|
model_reflections: model_reflections)
|
|
400
402
|
end
|
|
401
403
|
|
|
402
|
-
|
|
403
|
-
disabled_patterns = Array(config&.console_disabled_scanner_patterns)
|
|
404
|
-
scanner = if disabled_patterns.include?(:all)
|
|
405
|
-
nil
|
|
406
|
-
else
|
|
407
|
-
CredentialScanner.new(
|
|
408
|
-
disabled_patterns: disabled_patterns,
|
|
409
|
-
secret_index: secret_index
|
|
410
|
-
)
|
|
411
|
-
end
|
|
412
|
-
@active_scanner = scanner
|
|
404
|
+
scanner = build_scanner(config)
|
|
413
405
|
|
|
414
406
|
ResponseContext.build(safe_ctx: safe_ctx, table_gate: table_gate, credential_scanner: scanner)
|
|
415
407
|
end
|
|
416
408
|
|
|
409
|
+
def build_scanner(config)
|
|
410
|
+
disabled_patterns = Array(config&.console_disabled_scanner_patterns)
|
|
411
|
+
return nil if disabled_patterns.include?(:all)
|
|
412
|
+
|
|
413
|
+
SCANNERS.register do
|
|
414
|
+
CredentialScanner.new(disabled_patterns: disabled_patterns,
|
|
415
|
+
secret_index: build_credential_index(config))
|
|
416
|
+
end
|
|
417
|
+
end
|
|
418
|
+
|
|
417
419
|
# Build the boot-time credential index from Rails.application's encrypted
|
|
418
420
|
# credentials. Returns nil when credential defense is disabled or when no
|
|
419
421
|
# Rails application is reachable (specs, non-Rails hosts) — the scanner
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
# Metrics/ModuleLength is disabled here because the module body is almost
|
|
6
6
|
# entirely declarative data, not imperative logic. Decomposition would just
|
|
7
7
|
# scatter the tool catalogue across many files with no readability gain.
|
|
8
|
-
# rubocop:disable Metrics/ModuleLength
|
|
8
|
+
# rubocop:disable-next Metrics/ModuleLength
|
|
9
9
|
module Woods
|
|
10
10
|
module Console
|
|
11
11
|
module Server
|
|
@@ -549,4 +549,3 @@ module Woods
|
|
|
549
549
|
end
|
|
550
550
|
end
|
|
551
551
|
end
|
|
552
|
-
# rubocop:enable Metrics/ModuleLength
|
|
@@ -98,13 +98,12 @@ module Woods
|
|
|
98
98
|
# @param scope [Hash, nil] Filter conditions
|
|
99
99
|
# @param columns [Array<String>, nil] Columns to include
|
|
100
100
|
# @return [Hash] Bridge request
|
|
101
|
-
# rubocop:disable Metrics/ParameterLists
|
|
101
|
+
# rubocop:disable-next Metrics/ParameterLists
|
|
102
102
|
def console_recent(model:, order_by: 'created_at', direction: 'desc', limit: 10, scope: nil, columns: nil)
|
|
103
103
|
limit = [limit, 50].min
|
|
104
104
|
{ tool: 'recent', params: { model: model, order_by: order_by, direction: direction,
|
|
105
105
|
limit: limit, scope: scope, columns: columns }.compact }
|
|
106
106
|
end
|
|
107
|
-
# rubocop:enable Metrics/ParameterLists
|
|
108
107
|
|
|
109
108
|
# System health check.
|
|
110
109
|
#
|
|
@@ -61,7 +61,7 @@ module Woods
|
|
|
61
61
|
# @param scope [Hash, nil] Filter conditions
|
|
62
62
|
# @param limit [Integer, nil] Row limit (max 10000)
|
|
63
63
|
# @return [Hash] Bridge request
|
|
64
|
-
# rubocop:disable Metrics/ParameterLists
|
|
64
|
+
# rubocop:disable-next Metrics/ParameterLists
|
|
65
65
|
def console_query(model:, select:, joins: nil, group_by: nil, having: nil, order: nil, scope: nil, limit: nil)
|
|
66
66
|
limit = [limit, MAX_QUERY_LIMIT].min if limit
|
|
67
67
|
{
|
|
@@ -78,7 +78,6 @@ module Woods
|
|
|
78
78
|
}.compact
|
|
79
79
|
}
|
|
80
80
|
end
|
|
81
|
-
# rubocop:enable Metrics/ParameterLists
|
|
82
81
|
end
|
|
83
82
|
end
|
|
84
83
|
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'uri'
|
|
4
|
+
|
|
5
|
+
module Woods
|
|
6
|
+
module MCP
|
|
7
|
+
# Keep the SDK DNS-rebinding guard aligned with Woods' explicit allowlist.
|
|
8
|
+
# Empty configuration retains the SDK's strict loopback defaults.
|
|
9
|
+
module HttpTransportOptions
|
|
10
|
+
def self.for(origins)
|
|
11
|
+
normalized = Array(origins).map { |origin| origin.strip.downcase.delete_suffix('/') }.reject(&:empty?)
|
|
12
|
+
return {} if normalized.empty?
|
|
13
|
+
|
|
14
|
+
hosts = normalized.filter_map do |origin|
|
|
15
|
+
uri = URI.parse(origin)
|
|
16
|
+
uri.host if %w[http https].include?(uri.scheme)
|
|
17
|
+
rescue URI::InvalidURIError
|
|
18
|
+
nil
|
|
19
|
+
end
|
|
20
|
+
{ allowed_origins: normalized, allowed_hosts: hosts.uniq }
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
data/lib/woods/mcp/server.rb
CHANGED
|
@@ -93,15 +93,12 @@ module Woods
|
|
|
93
93
|
)
|
|
94
94
|
end
|
|
95
95
|
|
|
96
|
-
server =
|
|
96
|
+
server = VersionAwareServer.new(
|
|
97
97
|
name: 'woods',
|
|
98
98
|
version: Woods::VERSION,
|
|
99
99
|
resources: resources,
|
|
100
100
|
resource_templates: resource_templates
|
|
101
101
|
)
|
|
102
|
-
# Rewrite "Tool not found" into version-aware update guidance for agents
|
|
103
|
-
# running against an older gem than the skill they're following assumes.
|
|
104
|
-
server.singleton_class.prepend(VersionAwareToolDispatch)
|
|
105
102
|
|
|
106
103
|
define_lookup_tool(server, reader, respond, respond_err, renderer)
|
|
107
104
|
define_search_tool(server, reader, respond, respond_err, renderer)
|
|
@@ -200,11 +197,7 @@ module Woods
|
|
|
200
197
|
meta[:config_key] = config_key if config_key
|
|
201
198
|
meta[:doc_link] = doc_link if doc_link
|
|
202
199
|
meta.merge!(extra) unless extra.empty?
|
|
203
|
-
::MCP::Tool::Response.new(
|
|
204
|
-
[{ type: 'text', text: message }],
|
|
205
|
-
error: true,
|
|
206
|
-
meta: meta
|
|
207
|
-
)
|
|
200
|
+
::MCP::Tool::Response.new([{ type: 'text', text: message }], error: true, meta: meta)
|
|
208
201
|
end
|
|
209
202
|
|
|
210
203
|
def truncate_section(array, limit)
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require 'mcp'
|
|
3
4
|
require_relative '../update_check'
|
|
4
5
|
|
|
5
6
|
module Woods
|
|
6
7
|
module MCP
|
|
7
|
-
# Prepended
|
|
8
|
+
# Prepended before +MCP::Server+ initialization so that a call to a tool the
|
|
8
9
|
# installed gem does not define produces version-aware, self-healing
|
|
9
10
|
# guidance instead of a bare "Tool not found".
|
|
10
11
|
#
|
|
@@ -12,8 +13,7 @@ module Woods
|
|
|
12
13
|
# to call a tool that only exists in a later Woods release. The upstream
|
|
13
14
|
# +mcp+ gem raises +RequestHandlerError("Tool not found: X")+ with no seam to
|
|
14
15
|
# customize the message, so we intercept the built server's +call_tool+
|
|
15
|
-
#
|
|
16
|
-
# honored on both the stdio and HTTP transports) and re-raise with the
|
|
16
|
+
# before the SDK captures its bound handler method and re-raise with the
|
|
17
17
|
# installed version plus an update hint the agent can relay to the user.
|
|
18
18
|
#
|
|
19
19
|
# Only the genuine tool-not-found case is rewritten; every other
|
|
@@ -43,5 +43,11 @@ module Woods
|
|
|
43
43
|
error.error_type == :invalid_params && error.message.to_s.start_with?('Tool not found:')
|
|
44
44
|
end
|
|
45
45
|
end
|
|
46
|
+
|
|
47
|
+
# MCP 0.23 captures method(:call_tool) during initialization. The override
|
|
48
|
+
# must already exist then; a later singleton prepend misses that handler.
|
|
49
|
+
class VersionAwareServer < ::MCP::Server
|
|
50
|
+
prepend VersionAwareToolDispatch
|
|
51
|
+
end
|
|
46
52
|
end
|
|
47
53
|
end
|
|
@@ -30,9 +30,8 @@ module Woods
|
|
|
30
30
|
debug: /\b(bug|error|fix|broken|failing|wrong|issue|problem|crash|exception)\b/i,
|
|
31
31
|
implement: /\b(implement|add|create|build|write|make|generate)\b/i,
|
|
32
32
|
compare: /\b(compare|difference|vs|versus|between|contrast)\b/i,
|
|
33
|
-
# rubocop:disable Layout/LineLength
|
|
33
|
+
# rubocop:disable-next Layout/LineLength
|
|
34
34
|
framework: /\b(how does rails|what does rails|rails .+ work|work.+\brails\b|in rails\b|activerecord|actioncontroller|activejob|actionmailer|actioncable|actiontext|activestorage|solid_queue|solid_cache|solid_cable|kamal|propshaft|importmap|hotwire|turbo|stimulus|zeitwerk)\b/i,
|
|
35
|
-
# rubocop:enable Layout/LineLength
|
|
36
35
|
reference: /\b(show me|what is|what are|list|options for|api|interface|signature)\b/i,
|
|
37
36
|
understand: /\b(how|why|explain|understand|what happens|describe|overview)\b/i
|
|
38
37
|
}.freeze
|
|
@@ -85,7 +85,7 @@ module Woods
|
|
|
85
85
|
# @param env [Hash] Rack environment
|
|
86
86
|
# @param status [Integer] HTTP response status
|
|
87
87
|
# @param duration_ms [Integer] Request duration in milliseconds
|
|
88
|
-
# rubocop:disable Metrics/MethodLength
|
|
88
|
+
# rubocop:disable-next Metrics/MethodLength
|
|
89
89
|
def record_request(env, status, duration_ms)
|
|
90
90
|
path = env['PATH_INFO'] || ''
|
|
91
91
|
return if excluded?(path)
|
|
@@ -116,7 +116,6 @@ module Woods
|
|
|
116
116
|
|
|
117
117
|
@store.record(session_id, request_data)
|
|
118
118
|
end
|
|
119
|
-
# rubocop:enable Metrics/MethodLength
|
|
120
119
|
|
|
121
120
|
# Extract session ID: X-Trace-Session header first, then session cookie, then fallback.
|
|
122
121
|
#
|
|
@@ -24,7 +24,7 @@ module Woods
|
|
|
24
24
|
# doc = assembler.assemble("abc123", budget: 8000, depth: 1)
|
|
25
25
|
# puts doc.to_context
|
|
26
26
|
#
|
|
27
|
-
# rubocop:disable Metrics/ClassLength
|
|
27
|
+
# rubocop:disable-next Metrics/ClassLength
|
|
28
28
|
class SessionFlowAssembler
|
|
29
29
|
ASYNC_TYPES = %w[job mailer].to_set.freeze
|
|
30
30
|
|
|
@@ -41,7 +41,7 @@ module Woods
|
|
|
41
41
|
# @param budget [Integer] Maximum token budget (default: 8000)
|
|
42
42
|
# @param depth [Integer] Expansion depth (0=metadata only, 1=direct deps, 2+=full flow)
|
|
43
43
|
# @return [SessionFlowDocument] The assembled document
|
|
44
|
-
# rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength
|
|
44
|
+
# rubocop:disable-next Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength
|
|
45
45
|
def assemble(session_id, budget: 8000, depth: 1)
|
|
46
46
|
requests = @store.read(session_id)
|
|
47
47
|
return empty_document(session_id) if requests.empty?
|
|
@@ -89,7 +89,6 @@ module Woods
|
|
|
89
89
|
token_count: token_count
|
|
90
90
|
)
|
|
91
91
|
end
|
|
92
|
-
# rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength
|
|
93
92
|
|
|
94
93
|
private
|
|
95
94
|
|
|
@@ -115,7 +114,7 @@ module Woods
|
|
|
115
114
|
# Resolve dependencies for a unit, separating sync deps from async side effects.
|
|
116
115
|
#
|
|
117
116
|
# @return [Array<String>] Non-async dependency identifiers added
|
|
118
|
-
# rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/ParameterLists, Metrics/PerceivedComplexity
|
|
117
|
+
# rubocop:disable-next Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/ParameterLists, Metrics/PerceivedComplexity
|
|
119
118
|
def resolve_dependencies(unit_id, seen_units, context_pool,
|
|
120
119
|
side_effects, step, dependency_map, depth)
|
|
121
120
|
graph = @reader.dependency_graph
|
|
@@ -154,7 +153,6 @@ module Woods
|
|
|
154
153
|
|
|
155
154
|
added
|
|
156
155
|
end
|
|
157
|
-
# rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/ParameterLists, Metrics/PerceivedComplexity
|
|
158
156
|
|
|
159
157
|
# Expand transitive dependencies (depth 2+).
|
|
160
158
|
#
|
|
@@ -249,6 +247,5 @@ module Woods
|
|
|
249
247
|
SessionFlowDocument.new(session_id: session_id)
|
|
250
248
|
end
|
|
251
249
|
end
|
|
252
|
-
# rubocop:enable Metrics/ClassLength
|
|
253
250
|
end
|
|
254
251
|
end
|
|
@@ -23,7 +23,7 @@ module Woods
|
|
|
23
23
|
# doc.to_markdown # => human-readable document
|
|
24
24
|
# doc.to_context # => LLM XML format
|
|
25
25
|
#
|
|
26
|
-
# rubocop:disable Metrics/ClassLength
|
|
26
|
+
# rubocop:disable-next Metrics/ClassLength
|
|
27
27
|
class SessionFlowDocument
|
|
28
28
|
attr_reader :session_id, :steps, :context_pool, :side_effects,
|
|
29
29
|
:dependency_map, :token_count, :generated_at
|
|
@@ -35,7 +35,7 @@ module Woods
|
|
|
35
35
|
# @param dependency_map [Hash<String, Array<String>>] Unit -> dependency identifiers
|
|
36
36
|
# @param token_count [Integer] Estimated total tokens
|
|
37
37
|
# @param generated_at [String, nil] ISO8601 timestamp (defaults to now)
|
|
38
|
-
# rubocop:disable Metrics/ParameterLists
|
|
38
|
+
# rubocop:disable-next Metrics/ParameterLists
|
|
39
39
|
def initialize(session_id:, steps: [], context_pool: {}, side_effects: [],
|
|
40
40
|
dependency_map: {}, token_count: 0, generated_at: nil)
|
|
41
41
|
@session_id = session_id
|
|
@@ -46,7 +46,6 @@ module Woods
|
|
|
46
46
|
@token_count = token_count
|
|
47
47
|
@generated_at = generated_at || Time.now.utc.iso8601
|
|
48
48
|
end
|
|
49
|
-
# rubocop:enable Metrics/ParameterLists
|
|
50
49
|
|
|
51
50
|
# Serialize to a JSON-compatible Hash.
|
|
52
51
|
#
|
|
@@ -86,7 +85,7 @@ module Woods
|
|
|
86
85
|
# Render as human-readable Markdown.
|
|
87
86
|
#
|
|
88
87
|
# @return [String]
|
|
89
|
-
# rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
|
|
88
|
+
# rubocop:disable-next Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
|
|
90
89
|
def to_markdown
|
|
91
90
|
lines = []
|
|
92
91
|
lines << "## Session: #{@session_id}"
|
|
@@ -146,14 +145,13 @@ module Woods
|
|
|
146
145
|
|
|
147
146
|
lines.join("\n")
|
|
148
147
|
end
|
|
149
|
-
# rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
|
|
150
148
|
|
|
151
149
|
# Render as LLM-consumable XML context.
|
|
152
150
|
#
|
|
153
151
|
# Follows the format from docs/CONTEXT_AND_CHUNKING.md.
|
|
154
152
|
#
|
|
155
153
|
# @return [String]
|
|
156
|
-
# rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
|
|
154
|
+
# rubocop:disable-next Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
|
|
157
155
|
def to_context
|
|
158
156
|
lines = []
|
|
159
157
|
header = "<session_context session_id=\"#{@session_id}\" requests=\"#{@steps.size}\" " \
|
|
@@ -201,7 +199,6 @@ module Woods
|
|
|
201
199
|
lines << '</session_context>'
|
|
202
200
|
lines.join("\n")
|
|
203
201
|
end
|
|
204
|
-
# rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
|
|
205
202
|
|
|
206
203
|
# @api private
|
|
207
204
|
def self.deep_symbolize_keys(obj)
|
|
@@ -218,6 +215,5 @@ module Woods
|
|
|
218
215
|
end
|
|
219
216
|
private_class_method :deep_symbolize_keys
|
|
220
217
|
end
|
|
221
|
-
# rubocop:enable Metrics/ClassLength
|
|
222
218
|
end
|
|
223
219
|
end
|
data/lib/woods/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: woods
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.6.
|
|
4
|
+
version: 1.6.2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Leah Armstrong
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-
|
|
11
|
+
date: 2026-09-18 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: mcp
|
|
@@ -16,7 +16,7 @@ dependencies:
|
|
|
16
16
|
requirements:
|
|
17
17
|
- - ">="
|
|
18
18
|
- !ruby/object:Gem::Version
|
|
19
|
-
version: 0.
|
|
19
|
+
version: 0.23.0
|
|
20
20
|
- - "<"
|
|
21
21
|
- !ruby/object:Gem::Version
|
|
22
22
|
version: '1.0'
|
|
@@ -26,10 +26,30 @@ dependencies:
|
|
|
26
26
|
requirements:
|
|
27
27
|
- - ">="
|
|
28
28
|
- !ruby/object:Gem::Version
|
|
29
|
-
version: 0.
|
|
29
|
+
version: 0.23.0
|
|
30
30
|
- - "<"
|
|
31
31
|
- !ruby/object:Gem::Version
|
|
32
32
|
version: '1.0'
|
|
33
|
+
- !ruby/object:Gem::Dependency
|
|
34
|
+
name: json
|
|
35
|
+
requirement: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - ">="
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: 2.19.9
|
|
40
|
+
- - "<"
|
|
41
|
+
- !ruby/object:Gem::Version
|
|
42
|
+
version: '3'
|
|
43
|
+
type: :runtime
|
|
44
|
+
prerelease: false
|
|
45
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
46
|
+
requirements:
|
|
47
|
+
- - ">="
|
|
48
|
+
- !ruby/object:Gem::Version
|
|
49
|
+
version: 2.19.9
|
|
50
|
+
- - "<"
|
|
51
|
+
- !ruby/object:Gem::Version
|
|
52
|
+
version: '3'
|
|
33
53
|
- !ruby/object:Gem::Dependency
|
|
34
54
|
name: msgpack
|
|
35
55
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -37,6 +57,9 @@ dependencies:
|
|
|
37
57
|
- - ">="
|
|
38
58
|
- !ruby/object:Gem::Version
|
|
39
59
|
version: '1.5'
|
|
60
|
+
- - "<"
|
|
61
|
+
- !ruby/object:Gem::Version
|
|
62
|
+
version: '2'
|
|
40
63
|
type: :runtime
|
|
41
64
|
prerelease: false
|
|
42
65
|
version_requirements: !ruby/object:Gem::Requirement
|
|
@@ -44,6 +67,9 @@ dependencies:
|
|
|
44
67
|
- - ">="
|
|
45
68
|
- !ruby/object:Gem::Version
|
|
46
69
|
version: '1.5'
|
|
70
|
+
- - "<"
|
|
71
|
+
- !ruby/object:Gem::Version
|
|
72
|
+
version: '2'
|
|
47
73
|
- !ruby/object:Gem::Dependency
|
|
48
74
|
name: prism
|
|
49
75
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -65,6 +91,9 @@ dependencies:
|
|
|
65
91
|
- - ">="
|
|
66
92
|
- !ruby/object:Gem::Version
|
|
67
93
|
version: '6.0'
|
|
94
|
+
- - "<"
|
|
95
|
+
- !ruby/object:Gem::Version
|
|
96
|
+
version: '9'
|
|
68
97
|
type: :runtime
|
|
69
98
|
prerelease: false
|
|
70
99
|
version_requirements: !ruby/object:Gem::Requirement
|
|
@@ -72,6 +101,9 @@ dependencies:
|
|
|
72
101
|
- - ">="
|
|
73
102
|
- !ruby/object:Gem::Version
|
|
74
103
|
version: '6.0'
|
|
104
|
+
- - "<"
|
|
105
|
+
- !ruby/object:Gem::Version
|
|
106
|
+
version: '9'
|
|
75
107
|
description: |
|
|
76
108
|
Woods extracts structured data from Rails applications for use in
|
|
77
109
|
AI-assisted development tooling. It provides version-specific context by
|
|
@@ -133,8 +165,10 @@ files:
|
|
|
133
165
|
- lib/woods/console/console_response_renderer.rb
|
|
134
166
|
- lib/woods/console/credential_index.rb
|
|
135
167
|
- lib/woods/console/credential_scanner.rb
|
|
168
|
+
- lib/woods/console/credential_scanner_registry.rb
|
|
136
169
|
- lib/woods/console/dispatch_pipeline.rb
|
|
137
170
|
- lib/woods/console/embedded_executor.rb
|
|
171
|
+
- lib/woods/console/encrypted_credential_snapshot.rb
|
|
138
172
|
- lib/woods/console/eval_guard.rb
|
|
139
173
|
- lib/woods/console/model_validator.rb
|
|
140
174
|
- lib/woods/console/rack_middleware.rb
|
|
@@ -243,6 +277,7 @@ files:
|
|
|
243
277
|
- lib/woods/mcp/bootstrapper.rb
|
|
244
278
|
- lib/woods/mcp/config_resolver.rb
|
|
245
279
|
- lib/woods/mcp/errors.rb
|
|
280
|
+
- lib/woods/mcp/http_transport_options.rb
|
|
246
281
|
- lib/woods/mcp/index_reader.rb
|
|
247
282
|
- lib/woods/mcp/origin_guard.rb
|
|
248
283
|
- lib/woods/mcp/provider_probe.rb
|
|
@@ -324,10 +359,10 @@ licenses:
|
|
|
324
359
|
- MIT
|
|
325
360
|
metadata:
|
|
326
361
|
homepage_uri: https://github.com/lost-in-the/woods
|
|
327
|
-
source_code_uri: https://github.com/lost-in-the/woods/tree/
|
|
328
|
-
changelog_uri: https://github.com/lost-in-the/woods/blob/
|
|
362
|
+
source_code_uri: https://github.com/lost-in-the/woods/tree/v1.6.2
|
|
363
|
+
changelog_uri: https://github.com/lost-in-the/woods/blob/v1.6.2/CHANGELOG.md
|
|
329
364
|
bug_tracker_uri: https://github.com/lost-in-the/woods/issues
|
|
330
|
-
documentation_uri: https://github.com/lost-in-the/woods/tree/
|
|
365
|
+
documentation_uri: https://github.com/lost-in-the/woods/tree/v1.6.2/docs
|
|
331
366
|
rubygems_mfa_required: 'true'
|
|
332
367
|
post_install_message:
|
|
333
368
|
rdoc_options: []
|