woods 2.0.0 → 2.0.1
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 +21 -0
- data/CONTRIBUTING.md +12 -2
- data/docs/CONFIGURATION_REFERENCE.md +8 -1
- data/docs/CONSOLE_MCP_SETUP.md +85 -8
- data/docs/MCP_HTTP_TRANSPORT.md +54 -2
- data/docs/UPGRADING_TO_2.md +60 -0
- data/exe/woods-mcp-http +16 -9
- data/lib/woods/cache/cache_middleware.rb +12 -11
- data/lib/woods/console/adapter_family.rb +39 -0
- data/lib/woods/console/credential_index.rb +33 -3
- data/lib/woods/console/embedded_executor.rb +401 -43
- data/lib/woods/console/model_validator.rb +8 -0
- data/lib/woods/console/rack_middleware.rb +39 -10
- data/lib/woods/console/redactor.rb +24 -10
- data/lib/woods/console/safe_context.rb +44 -7
- data/lib/woods/console/sql_noise_stripper.rb +41 -12
- data/lib/woods/console/sql_table_scanner.rb +45 -34
- data/lib/woods/console/sql_validator.rb +37 -2
- data/lib/woods/mcp/bearer_auth.rb +1 -1
- data/lib/woods/mcp/origin_guard.rb +24 -77
- data/lib/woods/mcp/origin_policy.rb +124 -0
- data/lib/woods/railtie_support.rb +8 -0
- data/lib/woods/version.rb +1 -1
- data/plugin/skills/woods-diagnose/SKILL.md +11 -0
- data/plugin/skills/woods-mcp-config/SKILL.md +15 -0
- data/plugin/skills/woods-setup/SKILL.md +8 -0
- metadata +7 -5
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require 'json'
|
|
4
4
|
|
|
5
|
-
require_relative '
|
|
5
|
+
require_relative 'origin_policy'
|
|
6
6
|
|
|
7
7
|
module Woods
|
|
8
8
|
module MCP
|
|
@@ -21,17 +21,13 @@ module Woods
|
|
|
21
21
|
# also requiring Host to appear in the allow-list (or to be a loopback
|
|
22
22
|
# address), we close that gap even when Rails is bound to 0.0.0.0.
|
|
23
23
|
#
|
|
24
|
-
#
|
|
25
|
-
#
|
|
26
|
-
#
|
|
27
|
-
# matters.
|
|
24
|
+
# Cross-origin entries match exactly. A portless entry also permits
|
|
25
|
+
# same-authority requests on other ports; cross-port browser clients need
|
|
26
|
+
# their actual origin explicitly configured, as required by the SDK.
|
|
28
27
|
#
|
|
29
28
|
# Also answers CORS preflight (OPTIONS) with the matching allow-list.
|
|
30
29
|
class OriginGuard
|
|
31
|
-
DEFAULT_ALLOWED =
|
|
32
|
-
http://localhost http://127.0.0.1 http://[::1]
|
|
33
|
-
https://localhost https://127.0.0.1 https://[::1]
|
|
34
|
-
].freeze
|
|
30
|
+
DEFAULT_ALLOWED = OriginPolicy::DEFAULT_ORIGINS
|
|
35
31
|
|
|
36
32
|
# Hosts that always pass the Host-header check even without an explicit
|
|
37
33
|
# allow-list entry — they resolve to loopback by definition and cannot
|
|
@@ -56,6 +52,7 @@ module Woods
|
|
|
56
52
|
# which runs after Rails railtie initializers captured the middleware
|
|
57
53
|
# arguments — still takes effect (#183). Empty/nil falls back to
|
|
58
54
|
# {DEFAULT_ALLOWED}.
|
|
55
|
+
# @param policy [OriginPolicy, nil] Captured policy also passed to the SDK
|
|
59
56
|
# @param path [String, nil] When set, only requests whose PATH_INFO
|
|
60
57
|
# starts with this prefix are guarded — everything else passes
|
|
61
58
|
# straight through to the app. Nil (the default) guards every request.
|
|
@@ -82,27 +79,35 @@ module Woods
|
|
|
82
79
|
method = env['REQUEST_METHOD']
|
|
83
80
|
host = env['HTTP_HOST']
|
|
84
81
|
|
|
85
|
-
return forbidden
|
|
86
|
-
return forbidden_host
|
|
82
|
+
return forbidden unless policy.origin_allowed?(origin, host: host)
|
|
83
|
+
return forbidden_host unless policy.host_allowed?(host)
|
|
87
84
|
|
|
88
85
|
return preflight(origin) if method == 'OPTIONS'
|
|
89
86
|
|
|
90
87
|
status, headers, body = @app.call(env)
|
|
91
|
-
headers = cors_headers(origin).merge(headers) if origin
|
|
88
|
+
headers = cors_headers(origin).merge(headers) if origin
|
|
92
89
|
[status, headers, body]
|
|
93
90
|
end
|
|
94
91
|
|
|
92
|
+
# The same lazily captured immutable policy is passed to the transport.
|
|
93
|
+
# @return [OriginPolicy]
|
|
94
|
+
def policy
|
|
95
|
+
return @policy if @policy
|
|
96
|
+
|
|
97
|
+
@policy_mutex.synchronize do
|
|
98
|
+
@policy ||= OriginPolicy.new(allowed_origins: @allowed_source.call)
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
95
102
|
private
|
|
96
103
|
|
|
97
|
-
def initialize_options(app, allowed_origins: nil, path: nil, enabled: nil)
|
|
104
|
+
def initialize_options(app, allowed_origins: nil, path: nil, enabled: nil, policy: nil)
|
|
98
105
|
@app = app
|
|
99
106
|
@path = path
|
|
100
107
|
@enabled = enabled
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
build_allow_list(allowed_origins)
|
|
105
|
-
end
|
|
108
|
+
@policy = policy
|
|
109
|
+
@policy_mutex = Mutex.new
|
|
110
|
+
@allowed_source = allowed_origins.respond_to?(:call) ? allowed_origins : -> { allowed_origins }
|
|
106
111
|
end
|
|
107
112
|
|
|
108
113
|
# @param env [Hash] Rack environment
|
|
@@ -114,66 +119,8 @@ module Woods
|
|
|
114
119
|
true
|
|
115
120
|
end
|
|
116
121
|
|
|
117
|
-
# Normalize a raw allow-list into `@allowed` + `@allowed_hosts`.
|
|
118
|
-
#
|
|
119
|
-
# @param origins [Array<String>, nil]
|
|
120
|
-
# @return [Array<String>] the normalized allow-list
|
|
121
|
-
def build_allow_list(origins)
|
|
122
|
-
allowed = Array(origins).compact.reject { |o| o.to_s.strip.empty? }.map { |o| normalize(o) }
|
|
123
|
-
allowed = DEFAULT_ALLOWED.dup if allowed.empty?
|
|
124
|
-
@allowed_hosts = allowed.map { |o| extract_host(o) }.compact.uniq
|
|
125
|
-
@allowed = allowed
|
|
126
|
-
end
|
|
127
|
-
|
|
128
|
-
# The allow-list, resolving a deferred source on first use. Memoized —
|
|
129
|
-
# configuration is settled by the time the first request arrives.
|
|
130
|
-
#
|
|
131
|
-
# @return [Array<String>]
|
|
132
|
-
def allowed
|
|
133
|
-
@allowed || build_allow_list(@allowed_source.call)
|
|
134
|
-
end
|
|
135
|
-
|
|
136
|
-
# @return [Array<String>] hosts extracted from the allow-list
|
|
137
|
-
def allowed_hosts
|
|
138
|
-
allowed
|
|
139
|
-
@allowed_hosts
|
|
140
|
-
end
|
|
141
|
-
|
|
142
|
-
def normalize(origin)
|
|
143
|
-
origin.to_s.sub(%r{/\z}, '').downcase
|
|
144
|
-
end
|
|
145
|
-
|
|
146
|
-
def extract_host(origin)
|
|
147
|
-
host = origin.to_s.sub(%r{\Ahttps?://}, '').sub(%r{/.*\z}, '').downcase
|
|
148
|
-
host.empty? ? nil : host
|
|
149
|
-
end
|
|
150
|
-
|
|
151
|
-
def host_allowed?(host)
|
|
152
|
-
# Canonicalize (strip port, trailing dot, IPv6 brackets) via the
|
|
153
|
-
# shared helper so Qdrant and OriginGuard stay in sync on bypass
|
|
154
|
-
# notations. `normalized` keeps the port for literal allow-list
|
|
155
|
-
# lookups; `bare` drops it for loopback matching.
|
|
156
|
-
normalized = host.to_s.downcase.sub(/\.\z/, '')
|
|
157
|
-
bare = Util::HostGuard.canonicalize(host)
|
|
158
|
-
|
|
159
|
-
# Reject non-canonical numeric hosts. Net::HTTP / getaddrinfo
|
|
160
|
-
# would happily resolve `0x7f000001` or `2130706433` to 127.0.0.1,
|
|
161
|
-
# bypassing the loopback allow-list.
|
|
162
|
-
return false if Util::HostGuard.suspicious_numeric_host?(bare)
|
|
163
|
-
|
|
164
|
-
return true if LOOPBACK_HOSTS.include?(bare)
|
|
165
|
-
|
|
166
|
-
allowed_hosts.include?(normalized) || allowed_hosts.include?(bare)
|
|
167
|
-
end
|
|
168
|
-
|
|
169
|
-
def origin_allowed?(origin)
|
|
170
|
-
return false if origin.match?(/[[:cntrl:]]/)
|
|
171
|
-
|
|
172
|
-
allowed.include?(normalize(origin)) || allowed.include?(normalize(origin).sub(/:\d+\z/, ''))
|
|
173
|
-
end
|
|
174
|
-
|
|
175
122
|
def preflight(origin)
|
|
176
|
-
headers = origin
|
|
123
|
+
headers = origin ? cors_headers(origin) : {}
|
|
177
124
|
[204, headers, []]
|
|
178
125
|
end
|
|
179
126
|
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'uri'
|
|
4
|
+
require_relative '../util/host_guard'
|
|
5
|
+
|
|
6
|
+
module Woods
|
|
7
|
+
module MCP
|
|
8
|
+
# Immutable HTTP policy shared by Woods preflight and the MCP transport.
|
|
9
|
+
# Explicit cross-origin entries normalize HTTP(S) default ports. A portless entry also permits
|
|
10
|
+
# same-authority requests on another port, which the SDK accepts natively.
|
|
11
|
+
# No request header is rewritten and SDK rebinding protection stays enabled.
|
|
12
|
+
class OriginPolicy
|
|
13
|
+
LOOPBACK_HOSTS = %w[localhost 127.0.0.1 ::1].freeze
|
|
14
|
+
DEFAULT_ORIGINS = %w[
|
|
15
|
+
http://localhost http://127.0.0.1 http://[::1]
|
|
16
|
+
https://localhost https://127.0.0.1 https://[::1]
|
|
17
|
+
].freeze
|
|
18
|
+
|
|
19
|
+
# @param allowed_origins [Array<String>, nil] Explicit browser origins
|
|
20
|
+
def initialize(allowed_origins: nil)
|
|
21
|
+
entries = Array(allowed_origins).compact.reject do |entry|
|
|
22
|
+
entry.to_s.valid_encoding? && entry.to_s.strip.empty?
|
|
23
|
+
end
|
|
24
|
+
@explicit_origins = entries.map { |entry| configured_origin(entry) }.uniq.freeze
|
|
25
|
+
@allowed = (@explicit_origins.empty? ? DEFAULT_ORIGINS : @explicit_origins).freeze
|
|
26
|
+
@allowed_hosts = @explicit_origins.map do |origin|
|
|
27
|
+
host = authority(origin)
|
|
28
|
+
# SDK bare IPv6 host entries omit brackets; host:port entries retain
|
|
29
|
+
# them. URI#host alone retains brackets and loses explicit ports.
|
|
30
|
+
(host.end_with?(']') ? host[1...-1] : host).freeze
|
|
31
|
+
end.uniq.freeze
|
|
32
|
+
@transport_options = { allowed_origins: transport_origins, allowed_hosts: @allowed_hosts }.freeze
|
|
33
|
+
freeze
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# @return [Hash] Constructor options supported by the MCP SDK
|
|
37
|
+
attr_reader :transport_options
|
|
38
|
+
|
|
39
|
+
# @param host [String, nil] Unmodified HTTP Host header
|
|
40
|
+
# @return [Boolean] Whether the request authority is allowed
|
|
41
|
+
def host_allowed?(host)
|
|
42
|
+
return true if host.nil?
|
|
43
|
+
return false unless host.is_a?(String) && host.valid_encoding?
|
|
44
|
+
|
|
45
|
+
parsed = parsed_origin("http://#{host}")
|
|
46
|
+
return false unless parsed
|
|
47
|
+
|
|
48
|
+
hostname = parsed.hostname.downcase
|
|
49
|
+
return false if Util::HostGuard.suspicious_numeric_host?(hostname)
|
|
50
|
+
return true if LOOPBACK_HOSTS.include?(hostname)
|
|
51
|
+
|
|
52
|
+
@allowed_hosts.include?(host.downcase) || @allowed_hosts.include?(hostname)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# @param origin [String, nil] Unmodified HTTP Origin header
|
|
56
|
+
# @param host [String, nil] Unmodified HTTP Host header
|
|
57
|
+
# @return [Boolean] Whether preflight and SDK dispatch can both accept it
|
|
58
|
+
def origin_allowed?(origin, host:)
|
|
59
|
+
return true if origin.nil?
|
|
60
|
+
return false unless parsed_origin(origin)
|
|
61
|
+
|
|
62
|
+
normalized = normalized_origin(origin)
|
|
63
|
+
return true if @explicit_origins.include?(normalized)
|
|
64
|
+
return false unless @allowed.include?(normalized) || @allowed.include?(normalized.sub(/:\d+\z/, ''))
|
|
65
|
+
return false unless host.is_a?(String) && host.valid_encoding?
|
|
66
|
+
|
|
67
|
+
# Matches the SDK's same-authority rule. The origin's scheme selects
|
|
68
|
+
# its default port; request.scheme is unreliable behind reverse proxies.
|
|
69
|
+
default_port = normalized.start_with?('https://') ? ':443' : ':80'
|
|
70
|
+
authority(normalized).delete_suffix(default_port) == host.downcase.delete_suffix(default_port)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
private
|
|
74
|
+
|
|
75
|
+
def configured_origin(entry)
|
|
76
|
+
raw = entry.to_s
|
|
77
|
+
normalized = raw.downcase.sub(%r{/\z}, '') if raw.valid_encoding?
|
|
78
|
+
unless parsed_origin(normalized)
|
|
79
|
+
label = raw.b.byteslice(0, 160).inspect
|
|
80
|
+
raise ArgumentError, "Invalid MCP allowed origin #{label}: expected http(s)://host[:port] without a path"
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
normalized_origin(normalized).freeze
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def normalized_origin(origin)
|
|
87
|
+
value = origin.downcase
|
|
88
|
+
value.delete_suffix(value.start_with?('https://') ? ':443' : ':80')
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# The SDK compares configured origins literally. Include equivalent
|
|
92
|
+
# default-port spellings without rewriting the incoming request headers.
|
|
93
|
+
def transport_origins
|
|
94
|
+
@explicit_origins.flat_map do |origin|
|
|
95
|
+
parsed = parsed_origin(origin)
|
|
96
|
+
default = parsed.scheme == 'https' ? 443 : 80
|
|
97
|
+
parsed.port == default ? [origin, "#{origin}:#{default}"] : [origin]
|
|
98
|
+
end.uniq.map(&:freeze).freeze
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def authority(origin)
|
|
102
|
+
origin.sub(%r{\Ahttps?://}, '')
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# Parse only serialized HTTP origins, never URLs with paths, userinfo,
|
|
106
|
+
# query strings, fragments or whitespace. The comparison layer handles
|
|
107
|
+
# default-port equivalence separately from parsing.
|
|
108
|
+
def parsed_origin(origin)
|
|
109
|
+
return unless origin.is_a?(String) && origin.valid_encoding? && origin.ascii_only?
|
|
110
|
+
return if origin.match?(/[[:space:][:cntrl:]]/)
|
|
111
|
+
|
|
112
|
+
parsed = URI.parse(origin)
|
|
113
|
+
return unless %w[http https].include?(parsed.scheme&.downcase)
|
|
114
|
+
return unless parsed.host && !parsed.host.empty?
|
|
115
|
+
return unless parsed.path.to_s.empty? && !parsed.userinfo && !parsed.query && !parsed.fragment
|
|
116
|
+
return unless parsed.port&.between?(1, 65_535)
|
|
117
|
+
|
|
118
|
+
parsed
|
|
119
|
+
rescue URI::InvalidURIError, ArgumentError
|
|
120
|
+
nil
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
end
|
|
@@ -114,6 +114,7 @@ module Woods
|
|
|
114
114
|
warn_late_console_path(config)
|
|
115
115
|
return unless config.console_mcp_enabled && config.console_mcp_http_enabled
|
|
116
116
|
|
|
117
|
+
verify_console_origins!
|
|
117
118
|
require 'woods/mcp/bearer_auth'
|
|
118
119
|
token = config.console_mcp_token.to_s
|
|
119
120
|
if token.empty?
|
|
@@ -147,6 +148,13 @@ module Woods
|
|
|
147
148
|
|
|
148
149
|
private
|
|
149
150
|
|
|
151
|
+
def verify_console_origins!
|
|
152
|
+
require 'woods/mcp/origin_policy'
|
|
153
|
+
Woods::MCP::OriginPolicy.new(allowed_origins: config.console_mcp_allowed_origins)
|
|
154
|
+
rescue ArgumentError => e
|
|
155
|
+
raise Woods::ConfigurationError, "[Woods Console] #{e.message}"
|
|
156
|
+
end
|
|
157
|
+
|
|
150
158
|
# Warn when the configured console path no longer matches the path the
|
|
151
159
|
# stack was mounted at — the path was set after railtie initializers
|
|
152
160
|
# captured the middleware arguments.
|
data/lib/woods/version.rb
CHANGED
|
@@ -460,3 +460,14 @@ the named file before moving it aside, or choose a new export directory. Older v
|
|
|
460
460
|
byte-identical generated assets into `_woods/ownership.json`; changed legacy sidecars may need this
|
|
461
461
|
manual recovery. Never fabricate ownership receipts or remove personal files to silence the error.
|
|
462
462
|
See the installed version's `docs/OBSIDIAN_INTEGRATION.md` for the exact safety contract.
|
|
463
|
+
|
|
464
|
+
### Console read compatibility on security-patch candidates
|
|
465
|
+
|
|
466
|
+
Verify the loaded revision for corrections after 2.0.0. Active redaction refuses
|
|
467
|
+
SQL relation/CTE column alias lists; typed EAV matching can intentionally mask
|
|
468
|
+
extra values where tables share a final name or differ only by case. Keep the
|
|
469
|
+
policy enabled and use explicit scalar columns or structured tools. Check exact
|
|
470
|
+
sensitive-key spelling and configure binary secret columns for column redaction.
|
|
471
|
+
Read the [Console compatibility guide](https://github.com/lost-in-the/woods/blob/main/docs/CONSOLE_MCP_SETUP.md#read-policy-compatibility)
|
|
472
|
+
at the installed revision for adapter, timeout and projection limits; a plugin
|
|
473
|
+
update does not patch Woods.
|
|
@@ -214,3 +214,18 @@ returned typed, SHA-guarded `full_evidence` lookup for verification. Published-u
|
|
|
214
214
|
coordinates are not physical file offsets; unknown generation remains unknown.
|
|
215
215
|
Keep full-source access available. See the canonical
|
|
216
216
|
[evidence contract](https://github.com/lost-in-the/woods/blob/main/docs/RETRIEVAL_GUIDE.md#compact-published-evidence-and-api-outlines).
|
|
217
|
+
|
|
218
|
+
### HTTP configuration diagnostics on security-patch candidates
|
|
219
|
+
|
|
220
|
+
Check the installed revision before expecting this unreleased correction. Invalid
|
|
221
|
+
origin settings, including invalidly encoded entries, refuse at boot. The HTTP
|
|
222
|
+
executable reports one configuration diagnostic and exits 2. Correct the named
|
|
223
|
+
entry and restart; keep authentication and origin checks enabled. Follow the
|
|
224
|
+
installed revision's canonical `docs/MCP_HTTP_TRANSPORT.md` for accepted origins.
|
|
225
|
+
|
|
226
|
+
Use whitespace-free literal origin entries; explicit lists replace browser
|
|
227
|
+
defaults, and wildcard patterns are unsupported. Preserve the real Host behind
|
|
228
|
+
proxies. Verify both preflight and bearer-authenticated dispatch against the
|
|
229
|
+
[HTTP compatibility guide](https://github.com/lost-in-the/woods/blob/main/docs/MCP_HTTP_TRANSPORT.md#origin-configuration-compatibility)
|
|
230
|
+
at the installed revision; configured non-loopback Hosts are passed to the SDK
|
|
231
|
+
in supporting patches.
|
|
@@ -255,3 +255,11 @@ dependents and suggested tests manually; silence is not no impact. Do not clear
|
|
|
255
255
|
refresh queues when optional hints time out. See the canonical
|
|
256
256
|
[context guide](https://github.com/lost-in-the/woods/blob/main/docs/WATCH_DAEMON.md#optional-bounded-context-hints)
|
|
257
257
|
for output/time limits, container root mapping and emitted-hint suppression.
|
|
258
|
+
|
|
259
|
+
### Security-patch compatibility preflight
|
|
260
|
+
|
|
261
|
+
For security-patch candidates after 2.0.0, record the loaded revision and gem
|
|
262
|
+
path as well as the version. Review the [Console compatibility guide](https://github.com/lost-in-the/woods/blob/main/docs/CONSOLE_MCP_SETUP.md#read-policy-compatibility)
|
|
263
|
+
and HTTP origin compatibility at that revision before upgrading an authorized
|
|
264
|
+
Console installation. The plugin does not install these fixes or establish that
|
|
265
|
+
a release is published.
|
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: 2.0.
|
|
4
|
+
version: 2.0.1
|
|
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-09-
|
|
11
|
+
date: 2026-09-26 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: mcp
|
|
@@ -216,6 +216,7 @@ files:
|
|
|
216
216
|
- lib/woods/checks/moved_messages.rb
|
|
217
217
|
- lib/woods/chunking/chunk.rb
|
|
218
218
|
- lib/woods/chunking/semantic_chunker.rb
|
|
219
|
+
- lib/woods/console/adapter_family.rb
|
|
219
220
|
- lib/woods/console/audit_logger.rb
|
|
220
221
|
- lib/woods/console/bridge_protocol.rb
|
|
221
222
|
- lib/woods/console/confirmation.rb
|
|
@@ -376,6 +377,7 @@ files:
|
|
|
376
377
|
- lib/woods/mcp/index_reader_pinning.rb
|
|
377
378
|
- lib/woods/mcp/initialization_guidance.rb
|
|
378
379
|
- lib/woods/mcp/origin_guard.rb
|
|
380
|
+
- lib/woods/mcp/origin_policy.rb
|
|
379
381
|
- lib/woods/mcp/protocol_policy.rb
|
|
380
382
|
- lib/woods/mcp/provider_probe.rb
|
|
381
383
|
- lib/woods/mcp/published_lexical_retriever.rb
|
|
@@ -547,10 +549,10 @@ licenses:
|
|
|
547
549
|
- MIT
|
|
548
550
|
metadata:
|
|
549
551
|
homepage_uri: https://github.com/lost-in-the/woods
|
|
550
|
-
source_code_uri: https://github.com/lost-in-the/woods/tree/v2.0.
|
|
551
|
-
changelog_uri: https://github.com/lost-in-the/woods/blob/v2.0.
|
|
552
|
+
source_code_uri: https://github.com/lost-in-the/woods/tree/v2.0.1
|
|
553
|
+
changelog_uri: https://github.com/lost-in-the/woods/blob/v2.0.1/CHANGELOG.md
|
|
552
554
|
bug_tracker_uri: https://github.com/lost-in-the/woods/issues
|
|
553
|
-
documentation_uri: https://github.com/lost-in-the/woods/tree/v2.0.
|
|
555
|
+
documentation_uri: https://github.com/lost-in-the/woods/tree/v2.0.1/docs
|
|
554
556
|
rubygems_mfa_required: 'true'
|
|
555
557
|
post_install_message:
|
|
556
558
|
rdoc_options: []
|