woods 1.6.4 → 2.0.0.beta1
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 +1879 -37
- data/CONTRIBUTING.md +195 -137
- data/README.md +162 -520
- data/SECURITY.md +92 -0
- data/assets/woods-wordmark-white-with-bg.png +0 -0
- data/docs/AGENT_GUIDE.md +204 -0
- data/docs/AGENT_SETUP.md +205 -0
- data/docs/BACKEND_MATRIX.md +470 -0
- data/docs/CONFIGURATION_REFERENCE.md +620 -0
- data/docs/CONSOLE_MCP_SETUP.md +829 -0
- data/docs/DOCKER_SETUP.md +454 -0
- data/docs/EMBEDDING_MODELS.md +136 -0
- data/docs/EVALUATION.md +91 -0
- data/docs/EXTRACTOR_REFERENCE.md +765 -0
- data/docs/FAQ.md +544 -0
- data/docs/GETTING_STARTED.md +183 -0
- data/docs/INCREMENTAL_EXTRACTION.md +415 -0
- data/docs/INTERNALS.md +415 -0
- data/docs/MCP_HTTP_TRANSPORT.md +144 -0
- data/docs/MCP_SERVERS.md +231 -0
- data/docs/MCP_TOOL_COOKBOOK.md +987 -0
- data/docs/MCP_WORKTREE_SETUP.md +127 -0
- data/docs/NOTION_INTEGRATION.md +283 -0
- data/docs/OBSIDIAN_INTEGRATION.md +170 -0
- data/docs/PUBLISHED_INDEX.md +197 -0
- data/docs/README.md +94 -0
- data/docs/RETRIEVAL_GUIDE.md +267 -0
- data/docs/TOKEN_BENCHMARK.md +68 -0
- data/docs/TROUBLESHOOTING.md +841 -0
- data/docs/UNBLOCKED_INTEGRATION.md +279 -0
- data/docs/UPGRADING_TO_2.md +321 -0
- data/docs/WATCH_DAEMON.md +667 -0
- data/docs/WHY_WOODS.md +219 -0
- data/exe/woods-console +39 -3
- data/exe/woods-console-mcp +21 -35
- data/exe/woods-mcp +20 -7
- data/exe/woods-mcp-http +78 -24
- data/exe/woods-mcp-start +57 -52
- data/lib/generators/woods/install_generator.rb +6 -5
- data/lib/generators/woods/pgvector_generator.rb +6 -3
- data/lib/generators/woods/templates/add_pgvector_to_woods.rb.erb +29 -9
- data/lib/generators/woods/templates/create_woods_tables.rb.erb +5 -1
- data/lib/generators/woods/templates/woods.rb.tt +49 -28
- data/lib/tasks/woods.rake +622 -168
- data/lib/tasks/woods_checks.rake +107 -0
- data/lib/tasks/woods_evaluation.rake +164 -80
- data/lib/woods/ast/call_site_extractor.rb +6 -15
- data/lib/woods/ast/method_extractor.rb +19 -9
- data/lib/woods/ast/parser.rb +54 -8
- data/lib/woods/atomic_file.rb +40 -1
- data/lib/woods/builder.rb +310 -22
- data/lib/woods/cache/cache_middleware.rb +18 -13
- data/lib/woods/cache/cache_store.rb +9 -1
- data/lib/woods/cache/solid_cache_store.rb +6 -4
- data/lib/woods/change_set.rb +88 -0
- data/lib/woods/checks/generation_resolution.rb +34 -0
- data/lib/woods/checks/moved_messages.rb +186 -0
- data/lib/woods/chunking/semantic_chunker.rb +160 -18
- data/lib/woods/console/audit_logger.rb +12 -3
- data/lib/woods/console/bridge_protocol.rb +3 -16
- data/lib/woods/console/connection_manager.rb +51 -136
- data/lib/woods/console/credential_index.rb +5 -53
- data/lib/woods/console/credential_scanner.rb +15 -16
- data/lib/woods/console/dispatch_pipeline.rb +46 -34
- data/lib/woods/console/embedded_executor.rb +806 -257
- data/lib/woods/console/eval_guard.rb +27 -20
- data/lib/woods/console/input_contract.rb +78 -0
- data/lib/woods/console/model_validator.rb +24 -6
- data/lib/woods/console/rack_middleware.rb +62 -63
- data/lib/woods/console/redactor.rb +10 -24
- data/lib/woods/console/safe_context.rb +45 -45
- data/lib/woods/console/scope_predicate_parser.rb +41 -0
- data/lib/woods/console/server.rb +136 -267
- data/lib/woods/console/sql_noise_stripper.rb +20 -51
- data/lib/woods/console/sql_table_scanner.rb +39 -90
- data/lib/woods/console/sql_validator.rb +455 -85
- data/lib/woods/console/table_gate.rb +2 -2
- data/lib/woods/console/tool_specs.rb +462 -88
- data/lib/woods/console/tools/tier1.rb +0 -3
- data/lib/woods/console/tools/tier4.rb +17 -7
- data/lib/woods/coordination/lock_heartbeat.rb +103 -0
- data/lib/woods/coordination/pipeline_lock.rb +263 -53
- data/lib/woods/db/migrations/007_typed_snapshot_units.rb +45 -0
- data/lib/woods/db/migrator.rb +3 -9
- data/lib/woods/db/schema_version.rb +47 -2
- data/lib/woods/dependency_graph.rb +898 -64
- data/lib/woods/embedding/fake.rb +138 -0
- data/lib/woods/embedding/indexer.rb +832 -40
- data/lib/woods/embedding/openai.rb +77 -19
- data/lib/woods/embedding/provider.rb +189 -11
- data/lib/woods/embedding/text_preparer.rb +1 -1
- data/lib/woods/embedding/token_counter.rb +0 -7
- data/lib/woods/evaluation/ablation_agent_payload.rb +38 -0
- data/lib/woods/evaluation/ablation_executor.rb +67 -0
- data/lib/woods/evaluation/ablation_provenance.rb +38 -0
- data/lib/woods/evaluation/ablation_report_writer.rb +43 -0
- data/lib/woods/evaluation/ablation_runner.rb +173 -0
- data/lib/woods/evaluation/ablation_summary.rb +65 -0
- data/lib/woods/evaluation/ablation_task.rb +66 -0
- data/lib/woods/evaluation/ablation_task_set.rb +77 -0
- data/lib/woods/evaluation/ablation_timed_executor.rb +91 -0
- data/lib/woods/evaluation/ablation_worktree.rb +71 -0
- data/lib/woods/evaluation/baseline.rb +60 -0
- data/lib/woods/evaluation/baseline_runner.rb +11 -3
- data/lib/woods/evaluation/evaluator.rb +41 -8
- data/lib/woods/evaluation/query_set.rb +79 -13
- data/lib/woods/evaluation/report_generator.rb +20 -1
- data/lib/woods/export/unit_facts.rb +0 -11
- data/lib/woods/extracted_unit.rb +22 -63
- data/lib/woods/extractor.rb +2503 -192
- data/lib/woods/extractors/action_cable_extractor.rb +9 -4
- data/lib/woods/extractors/ast_source_extraction.rb +20 -2
- data/lib/woods/extractors/caching_extractor.rb +46 -12
- data/lib/woods/extractors/callback_analyzer.rb +39 -9
- data/lib/woods/extractors/component_discovery.rb +123 -0
- data/lib/woods/extractors/concern_extractor.rb +17 -3
- data/lib/woods/extractors/controller_extractor.rb +389 -29
- data/lib/woods/extractors/decorator_extractor.rb +7 -14
- data/lib/woods/extractors/engine_extractor.rb +53 -8
- data/lib/woods/extractors/event_extractor.rb +55 -4
- data/lib/woods/extractors/factory_extractor.rb +49 -11
- data/lib/woods/extractors/graphql_extractor.rb +162 -66
- data/lib/woods/extractors/i18n_extractor.rb +6 -1
- data/lib/woods/extractors/job_extractor.rb +51 -21
- data/lib/woods/extractors/lib_extractor.rb +23 -17
- data/lib/woods/extractors/line_neutralizer.rb +171 -0
- data/lib/woods/extractors/mailer_extractor.rb +9 -1
- data/lib/woods/extractors/manager_extractor.rb +19 -2
- data/lib/woods/extractors/migration_extractor.rb +22 -11
- data/lib/woods/extractors/model_extractor.rb +292 -57
- data/lib/woods/extractors/package_extractor.rb +154 -0
- data/lib/woods/extractors/phlex_extractor.rb +18 -3
- data/lib/woods/extractors/policy_extractor.rb +6 -5
- data/lib/woods/extractors/poro_extractor.rb +13 -14
- data/lib/woods/extractors/pundit_extractor.rb +3 -3
- data/lib/woods/extractors/rails_source_extractor.rb +24 -7
- data/lib/woods/extractors/rake_task_extractor.rb +158 -30
- data/lib/woods/extractors/reference_patterns.rb +38 -0
- data/lib/woods/extractors/route_extractor.rb +58 -2
- data/lib/woods/extractors/scheduled_job_extractor.rb +51 -35
- data/lib/woods/extractors/serializer_extractor.rb +3 -4
- data/lib/woods/extractors/service_extractor.rb +11 -1
- data/lib/woods/extractors/shared_dependency_scanner.rb +24 -34
- data/lib/woods/extractors/shared_utility_methods.rb +36 -6
- data/lib/woods/extractors/source_nesting.rb +560 -0
- data/lib/woods/extractors/state_machine_extractor.rb +30 -18
- data/lib/woods/extractors/test_mapping_extractor.rb +26 -9
- data/lib/woods/extractors/view_component_extractor.rb +28 -3
- data/lib/woods/extractors/view_engines/erb.rb +17 -3
- data/lib/woods/feedback/gap_detector.rb +9 -3
- data/lib/woods/feedback/store.rb +7 -1
- data/lib/woods/filename_utils.rb +29 -1
- data/lib/woods/flow_analysis/operation_extractor.rb +22 -10
- data/lib/woods/flow_assembler.rb +63 -21
- data/lib/woods/flow_document.rb +1 -0
- data/lib/woods/flow_precomputer.rb +138 -22
- data/lib/woods/gem_mapper.rb +285 -0
- data/lib/woods/generation.rb +185 -0
- data/lib/woods/git_command.rb +38 -0
- data/lib/woods/git_provenance.rb +16 -2
- data/lib/woods/graph_analyzer.rb +408 -34
- data/lib/woods/index_artifact.rb +93 -23
- data/lib/woods/mcp/bearer_auth.rb +92 -22
- data/lib/woods/mcp/bootstrap_state.rb +77 -0
- data/lib/woods/mcp/bootstrapper.rb +582 -77
- data/lib/woods/mcp/config_resolver.rb +66 -6
- data/lib/woods/mcp/errors.rb +60 -0
- data/lib/woods/mcp/index_reader.rb +836 -117
- data/lib/woods/mcp/index_reader_pinning.rb +78 -0
- data/lib/woods/mcp/origin_guard.rb +108 -23
- data/lib/woods/mcp/protocol_policy.rb +98 -0
- data/lib/woods/mcp/provider_probe.rb +45 -6
- data/lib/woods/mcp/renderers/markdown_renderer.rb +72 -4
- data/lib/woods/mcp/renderers/plain_renderer.rb +54 -6
- data/lib/woods/mcp/server.rb +907 -154
- data/lib/woods/mcp/tasks/extension.rb +196 -0
- data/lib/woods/mcp/tasks/request_capture.rb +45 -0
- data/lib/woods/mcp/tasks/store.rb +518 -0
- data/lib/woods/mcp/tool_contract.rb +171 -0
- data/lib/woods/mcp/tool_response_renderer.rb +7 -0
- data/lib/woods/mcp/version_aware_tool_dispatch.rb +3 -9
- data/lib/woods/model_name_cache.rb +19 -1
- data/lib/woods/notion/client.rb +132 -36
- data/lib/woods/notion/exporter.rb +456 -61
- data/lib/woods/notion/mappers/column_mapper.rb +34 -5
- data/lib/woods/notion/mappers/migration_mapper.rb +32 -8
- data/lib/woods/notion/mappers/model_mapper.rb +21 -6
- data/lib/woods/notion/mappers/shared.rb +45 -3
- data/lib/woods/notion/sync_manifest.rb +258 -0
- data/lib/woods/obsidian/errors.rb +6 -0
- data/lib/woods/obsidian/name_mapper.rb +40 -24
- data/lib/woods/obsidian/vault_exporter.rb +103 -36
- data/lib/woods/operator/pipeline_guard.rb +118 -21
- data/lib/woods/operator/status_reporter.rb +20 -3
- data/lib/woods/path_dispatcher.rb +276 -0
- data/lib/woods/payload_store.rb +223 -0
- data/lib/woods/published_index/edge_shaper.rb +61 -0
- data/lib/woods/published_index/generation_catalog.rb +72 -0
- data/lib/woods/published_index/typed_unit_reader.rb +48 -0
- data/lib/woods/published_index.rb +287 -0
- data/lib/woods/railtie.rb +70 -38
- data/lib/woods/railtie_support.rb +167 -0
- data/lib/woods/release.rb +12 -0
- data/lib/woods/reload_policy.rb +206 -0
- data/lib/woods/resilience/circuit_breaker.rb +47 -8
- data/lib/woods/resilience/index_validator.rb +296 -10
- data/lib/woods/resilience/retryable_provider.rb +71 -6
- data/lib/woods/resolved_config.rb +55 -11
- data/lib/woods/retrieval/context_assembler.rb +132 -40
- data/lib/woods/retrieval/query_classifier.rb +25 -6
- data/lib/woods/retrieval/ranker.rb +193 -28
- data/lib/woods/retrieval/search_executor.rb +206 -39
- data/lib/woods/retriever.rb +317 -71
- data/lib/woods/retry_after.rb +22 -2
- data/lib/woods/ruby_analyzer/class_analyzer.rb +10 -14
- data/lib/woods/ruby_analyzer/fqn_builder.rb +2 -0
- data/lib/woods/ruby_analyzer/mermaid_renderer.rb +14 -4
- data/lib/woods/ruby_analyzer/method_analyzer.rb +1 -1
- data/lib/woods/ruby_analyzer.rb +21 -5
- data/lib/woods/session_tracer/file_store.rb +138 -19
- data/lib/woods/session_tracer/redis_store.rb +122 -12
- data/lib/woods/session_tracer/session_flow_assembler.rb +54 -11
- data/lib/woods/session_tracer/session_flow_document.rb +52 -6
- data/lib/woods/session_tracer/solid_cache_coordination.rb +192 -0
- data/lib/woods/session_tracer/solid_cache_store.rb +560 -91
- data/lib/woods/session_tracer/store.rb +14 -1
- data/lib/woods/storage/metadata_store.rb +230 -26
- data/lib/woods/storage/pgvector.rb +180 -22
- data/lib/woods/storage/qdrant.rb +367 -41
- data/lib/woods/storage/snapshotter/metadata.rb +79 -16
- data/lib/woods/storage/snapshotter/vector.rb +128 -17
- data/lib/woods/storage/snapshotter.rb +23 -5
- data/lib/woods/storage/vector_store.rb +49 -8
- data/lib/woods/storage_identity.rb +28 -0
- data/lib/woods/tasks.rb +53 -2
- data/lib/woods/temporal/json_snapshot_store.rb +112 -42
- data/lib/woods/temporal/snapshot_store.rb +139 -42
- data/lib/woods/unblocked/client.rb +119 -17
- data/lib/woods/unblocked/document_builder.rb +34 -2
- data/lib/woods/unblocked/exporter.rb +63 -27
- data/lib/woods/unblocked/rate_limiter.rb +23 -9
- data/lib/woods/unblocked/sync_manifest.rb +16 -8
- data/lib/woods/update_check.rb +24 -1
- data/lib/woods/util/uuid5.rb +124 -0
- data/lib/woods/version.rb +1 -1
- data/lib/woods/watch/daemon.rb +1345 -0
- data/lib/woods/watch/listen_watcher.rb +81 -0
- data/lib/woods/watch/polling_watcher.rb +137 -0
- data/lib/woods/watch/status.rb +169 -0
- data/lib/woods/watch/tree_scan.rb +163 -0
- data/lib/woods/watch/watcher.rb +100 -0
- data/lib/woods.rb +53 -9
- data/plugin/.claude-plugin/plugin.json +18 -0
- data/plugin/hooks/hooks.json +29 -0
- data/plugin/hooks/woods-post-edit.sh +226 -0
- data/plugin/hooks/woods-session-start.sh +77 -0
- data/plugin/skills/woods-agent-enable/SKILL.md +51 -0
- data/plugin/skills/woods-diagnose/SKILL.md +75 -0
- data/plugin/skills/woods-investigate/SKILL.md +39 -0
- data/plugin/skills/woods-mcp-config/SKILL.md +101 -0
- data/plugin/skills/woods-setup/SKILL.md +99 -0
- metadata +102 -30
- data/lib/woods/console/adapter_family.rb +0 -39
- data/lib/woods/console/adapters/cache_adapter.rb +0 -58
- data/lib/woods/console/adapters/good_job_adapter.rb +0 -33
- data/lib/woods/console/adapters/job_adapter.rb +0 -74
- data/lib/woods/console/adapters/sidekiq_adapter.rb +0 -33
- data/lib/woods/console/adapters/solid_queue_adapter.rb +0 -33
- data/lib/woods/console/bridge.rb +0 -210
- data/lib/woods/console/credential_scanner_registry.rb +0 -36
- data/lib/woods/console/encrypted_credential_snapshot.rb +0 -16
- data/lib/woods/console/sql_output_policy.rb +0 -535
- data/lib/woods/console/sqlite_read_guard.rb +0 -46
- data/lib/woods/formatting/claude_adapter.rb +0 -98
- data/lib/woods/formatting/generic_adapter.rb +0 -56
- data/lib/woods/formatting/gpt_adapter.rb +0 -64
- data/lib/woods/mcp/http_transport_options.rb +0 -15
- data/lib/woods/mcp/origin_policy.rb +0 -113
- data/lib/woods/notion/mapper.rb +0 -40
- data/lib/woods/observability/health_check.rb +0 -79
- data/lib/woods/observability/instrumentation.rb +0 -34
|
@@ -39,6 +39,15 @@ module Woods
|
|
|
39
39
|
# Suffix pattern — longest suffix match wins because we scan the full list.
|
|
40
40
|
SUFFIX_PATTERN = /(_eq|_not_eq|_gteq|_lteq|_gt|_lt|_not_in|_not_null|_in|_null|_present|_blank|_matches)\z/
|
|
41
41
|
|
|
42
|
+
# Suffix classes, each with its own required value type. Ruby truthiness
|
|
43
|
+
# treats any non-nil, non-false value as true — so without a strict type
|
|
44
|
+
# check, `{status_present: "false"}` (a JSON string) inverts the caller's
|
|
45
|
+
# intent instead of raising. `_eq`/`_not_eq`/`_matches` accept any scalar
|
|
46
|
+
# and are intentionally not classified here.
|
|
47
|
+
EXISTENCE_SUFFIXES = %w[_null _not_null _present _blank].freeze
|
|
48
|
+
COMPARISON_SUFFIXES = %w[_gt _gteq _lt _lteq].freeze
|
|
49
|
+
SET_SUFFIXES = %w[_in _not_in].freeze
|
|
50
|
+
|
|
42
51
|
SUFFIX_HINT = "Supported suffixes: #{SUPPORTED_SUFFIXES.join(', ')}.".freeze
|
|
43
52
|
|
|
44
53
|
# @param model_name [String] ActiveRecord model name (e.g. 'Order')
|
|
@@ -70,8 +79,10 @@ module Woods
|
|
|
70
79
|
suffix = match[1]
|
|
71
80
|
column = key.delete_suffix(suffix)
|
|
72
81
|
@model_validator.validate_column!(@model_name, column)
|
|
82
|
+
validate_suffix_value_type!(suffix, value)
|
|
73
83
|
arel_nodes << build_node(relation, column, suffix, value)
|
|
74
84
|
else
|
|
85
|
+
@model_validator.validate_column!(@model_name, key)
|
|
75
86
|
equality[raw_key] = value
|
|
76
87
|
end
|
|
77
88
|
end
|
|
@@ -83,6 +94,36 @@ module Woods
|
|
|
83
94
|
|
|
84
95
|
private
|
|
85
96
|
|
|
97
|
+
# Enforce the suffix-dependent value type before any Arel node is
|
|
98
|
+
# built. `true`/`false` are checked by identity (`==`), not truthiness,
|
|
99
|
+
# so a JSON string like `"false"` is rejected rather than silently
|
|
100
|
+
# treated as truthy.
|
|
101
|
+
#
|
|
102
|
+
# @param suffix [String] One of SUPPORTED_SUFFIXES
|
|
103
|
+
# @param value [Object] The predicate value
|
|
104
|
+
# @raise [ValidationError] when the value's type doesn't match the suffix class
|
|
105
|
+
def validate_suffix_value_type!(suffix, value)
|
|
106
|
+
case suffix
|
|
107
|
+
when *EXISTENCE_SUFFIXES
|
|
108
|
+
return if [true, false].include?(value)
|
|
109
|
+
|
|
110
|
+
raise ValidationError,
|
|
111
|
+
"Predicate suffix '#{suffix}' requires a strict boolean value " \
|
|
112
|
+
"(got #{value.class}: #{value.inspect}). #{SUFFIX_HINT}"
|
|
113
|
+
when *COMPARISON_SUFFIXES
|
|
114
|
+
return if value.is_a?(String) || value.is_a?(Numeric)
|
|
115
|
+
|
|
116
|
+
raise ValidationError,
|
|
117
|
+
"Predicate suffix '#{suffix}' requires a scalar (String or Numeric) value " \
|
|
118
|
+
"(got #{value.class}: #{value.inspect}). #{SUFFIX_HINT}"
|
|
119
|
+
end
|
|
120
|
+
# _in/_not_in are intentionally not type-checked here: build_node
|
|
121
|
+
# already wraps a scalar in a single-element Array via `Array(value)`,
|
|
122
|
+
# a pre-existing, separately-tested convenience for callers building
|
|
123
|
+
# scope hashes programmatically. The public JSON Schema still
|
|
124
|
+
# requires a real array for _in/_not_in at the MCP boundary.
|
|
125
|
+
end
|
|
126
|
+
|
|
86
127
|
# Build an Arel predicate node for a validated column + suffix.
|
|
87
128
|
#
|
|
88
129
|
# @param relation [ActiveRecord::Relation, Class] Used to get the arel_table
|
data/lib/woods/console/server.rb
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require 'mcp'
|
|
4
|
+
require_relative '../mcp/protocol_policy'
|
|
4
5
|
require_relative 'connection_manager'
|
|
5
6
|
require_relative 'model_validator'
|
|
6
7
|
require_relative 'safe_context'
|
|
@@ -13,7 +14,6 @@ require_relative 'audit_logger'
|
|
|
13
14
|
require_relative 'confirmation'
|
|
14
15
|
require_relative 'console_response_renderer'
|
|
15
16
|
require_relative 'credential_scanner'
|
|
16
|
-
require_relative 'credential_scanner_registry'
|
|
17
17
|
require_relative 'eval_guard'
|
|
18
18
|
require_relative 'table_gate'
|
|
19
19
|
require_relative 'redactor'
|
|
@@ -22,38 +22,38 @@ require_relative 'dispatch_pipeline'
|
|
|
22
22
|
require_relative 'tool_specs'
|
|
23
23
|
|
|
24
24
|
module Woods
|
|
25
|
+
# Same conditional-define pattern used elsewhere in the gem (e.g.
|
|
26
|
+
# Storage::MetadataStore) so this file can be required in isolation
|
|
27
|
+
# without tripping NameError on the raise in Server.build below.
|
|
28
|
+
class Error < StandardError; end unless defined?(Woods::Error)
|
|
29
|
+
class ConfigurationError < Error; end unless defined?(Woods::ConfigurationError)
|
|
30
|
+
|
|
25
31
|
module Console
|
|
26
32
|
# Console MCP Server — queries live Rails application state.
|
|
27
33
|
#
|
|
28
|
-
#
|
|
29
|
-
#
|
|
30
|
-
#
|
|
34
|
+
# Executes against a booted Rails application. The default embedded mode
|
|
35
|
+
# exposes 9 Tier 1 tools; explicit read-tool mode adds console_sql and
|
|
36
|
+
# console_query. The full 31-tool catalogue remains inventory-only.
|
|
31
37
|
#
|
|
32
38
|
# @example
|
|
33
|
-
# server = Woods::Console::Server.
|
|
39
|
+
# server = Woods::Console::Server.build_embedded(
|
|
40
|
+
# model_validator: validator,
|
|
41
|
+
# safe_context: safe_context
|
|
42
|
+
# )
|
|
34
43
|
# transport = MCP::Server::Transports::StdioTransport.new(server)
|
|
35
44
|
# transport.open
|
|
36
45
|
#
|
|
37
46
|
module Server # rubocop:disable Metrics/ModuleLength
|
|
38
|
-
SCANNERS = CredentialScannerRegistry.new
|
|
39
|
-
private_constant :SCANNERS
|
|
40
|
-
|
|
41
47
|
class << self # rubocop:disable Metrics/ClassLength
|
|
42
48
|
# Rebuild the boot-time credential index from fresh Rails credentials
|
|
43
|
-
# and hot-swap it into
|
|
49
|
+
# and hot-swap it into the active scanner without restarting the process.
|
|
44
50
|
#
|
|
45
|
-
# Refresh failures propagate without replacing any live index.
|
|
46
51
|
# Host rotation jobs should call this immediately after `rails credentials:edit`
|
|
47
52
|
# changes are deployed. The swap is atomic on MRI (GVL) — in-flight scans see
|
|
48
53
|
# either the old or the new index, never a partial one.
|
|
49
54
|
#
|
|
50
|
-
# Returns nil when
|
|
51
|
-
#
|
|
52
|
-
# - No server has been built yet in this process (`build` / `build_embedded`
|
|
53
|
-
# have not been called)
|
|
54
|
-
#
|
|
55
|
-
# Existing callers of `build` / `build_embedded` are unaffected — this is an
|
|
56
|
-
# additive class method with no required arguments beyond `rails_app`.
|
|
55
|
+
# Returns nil when credential defense is disabled or no embedded server
|
|
56
|
+
# has been built yet in this process.
|
|
57
57
|
#
|
|
58
58
|
# @param rails_app [#credentials] The Rails application to re-read.
|
|
59
59
|
# Defaults to `Rails.application` when `Rails` is defined, otherwise
|
|
@@ -62,11 +62,14 @@ module Woods
|
|
|
62
62
|
# the rebuild was skipped.
|
|
63
63
|
def rebuild_credential_index(rails_app: nil)
|
|
64
64
|
return nil unless credential_defense_enabled?
|
|
65
|
+
return nil unless @active_scanner
|
|
65
66
|
|
|
66
67
|
target_app = rails_app || default_rails_app
|
|
67
68
|
return nil unless target_app
|
|
68
69
|
|
|
69
|
-
|
|
70
|
+
new_index = CredentialIndex.build(rails_app: target_app)
|
|
71
|
+
@active_scanner.replace_index!(new_index)
|
|
72
|
+
new_index
|
|
70
73
|
end
|
|
71
74
|
|
|
72
75
|
# True when Woods is configured and credential defense is on.
|
|
@@ -75,15 +78,8 @@ module Woods
|
|
|
75
78
|
config&.console_credential_defense_enabled ? true : false
|
|
76
79
|
end
|
|
77
80
|
|
|
78
|
-
# True when
|
|
79
|
-
#
|
|
80
|
-
# `config.console_unsafe_eval_enabled = true`. Explicit config wins
|
|
81
|
-
# over the env var in both directions.
|
|
82
|
-
#
|
|
83
|
-
# NOTE: returning true here does NOT enable eval execution. The
|
|
84
|
-
# execution path is deliberately unimplemented (backlog
|
|
85
|
-
# unsafe-eval-opt-in). This predicate only governs the boot-time
|
|
86
|
-
# banner and the production-environment refusal below.
|
|
81
|
+
# True when legacy configuration requests the unsupported eval path.
|
|
82
|
+
# Explicit config wins over the environment in both directions.
|
|
87
83
|
#
|
|
88
84
|
# @return [Boolean]
|
|
89
85
|
def unsafe_eval_enabled?
|
|
@@ -94,29 +90,13 @@ module Woods
|
|
|
94
90
|
ENV['WOODS_CONSOLE_UNSAFE_EVAL'] == 'true'
|
|
95
91
|
end
|
|
96
92
|
|
|
97
|
-
#
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
# - refuse outright in `Rails.env.production?` (non-negotiable),
|
|
101
|
-
# - otherwise emit a LOUD stderr banner so operators know the flag
|
|
102
|
-
# is live even though eval remains unimplemented.
|
|
103
|
-
#
|
|
104
|
-
# Safe when Rails is not loaded (specs, non-Rails hosts).
|
|
105
|
-
#
|
|
106
|
-
# @return [void]
|
|
107
|
-
# @raise [Woods::ConfigurationError] when the flag is on in production.
|
|
108
|
-
def enforce_unsafe_eval_contract!
|
|
109
|
-
return unless unsafe_eval_enabled?
|
|
110
|
-
|
|
111
|
-
if defined?(Rails) && Rails.respond_to?(:env) && Rails.env.respond_to?(:production?) &&
|
|
112
|
-
Rails.env.production?
|
|
113
|
-
raise Woods::ConfigurationError,
|
|
114
|
-
'WOODS_CONSOLE_UNSAFE_EVAL is set but Rails.env.production? is true. ' \
|
|
115
|
-
'console_eval cannot be opted into in production. Unset the flag or ' \
|
|
116
|
-
'restart in a non-production environment.'
|
|
117
|
-
end
|
|
93
|
+
# Fail closed because no supported MCP mode registers console_eval.
|
|
94
|
+
def enforce_unsafe_eval_contract!(legacy_options_present: false)
|
|
95
|
+
return unless unsafe_eval_enabled? || legacy_options_present
|
|
118
96
|
|
|
119
|
-
|
|
97
|
+
raise Woods::ConfigurationError,
|
|
98
|
+
'WOODS_CONSOLE_UNSAFE_EVAL is set, but console_eval is not available in a ' \
|
|
99
|
+
'supported Console MCP mode. Unset the flag; use console_query or console_sql.'
|
|
120
100
|
end
|
|
121
101
|
|
|
122
102
|
# Resolves `Rails.application` when available, else nil.
|
|
@@ -126,36 +106,15 @@ module Woods
|
|
|
126
106
|
Rails.application
|
|
127
107
|
end
|
|
128
108
|
|
|
129
|
-
#
|
|
130
|
-
#
|
|
131
|
-
#
|
|
132
|
-
# The server side of the bridge has no access to the remote app's
|
|
133
|
-
# `ActiveRecord::Base.descendants`, so model_tables and model_reflections
|
|
134
|
-
# are empty. `TableGate#check_sql!` still fires against the raw SQL
|
|
135
|
-
# argument of `console_sql`, but `check_model!`, `check_joins!`, and
|
|
136
|
-
# `check_association!` are effectively no-ops for tools that receive a
|
|
137
|
-
# model name rather than SQL (find, sample, count, etc.). A bridge-mode
|
|
138
|
-
# deployment therefore relies on Layer 2 (credential scanning) + Layer 3
|
|
139
|
-
# (column/EAV redaction) + Layer 4 (SqlValidator + SafeContext rollback)
|
|
140
|
-
# for non-SQL tool calls. If you need full Layer 1 coverage, use
|
|
141
|
-
# `build_embedded` (the stdio entry point `exe/woods-console` does this).
|
|
142
|
-
#
|
|
143
|
-
# See docs/CONSOLE_MCP_SETUP.md "Bridge vs. embedded defense coverage"
|
|
144
|
-
# for the full matrix.
|
|
145
|
-
#
|
|
109
|
+
# The former JSON-lines bridge never executed live queries. Keep this
|
|
110
|
+
# entry point fail-closed so legacy callers cannot receive fabricated
|
|
111
|
+
# empty responses.
|
|
146
112
|
# @param config [Hash] Configuration hash (from YAML or env)
|
|
147
|
-
# @
|
|
148
|
-
def build(config:)
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
redacted_key_values = Array(
|
|
153
|
-
config['redacted_key_values'] || connection_config['redacted_key_values']
|
|
154
|
-
)
|
|
155
|
-
safe_ctx = build_safe_context(redacted_columns, redacted_key_values)
|
|
156
|
-
ctx = build_response_context(safe_ctx: safe_ctx, model_tables: {}, model_reflections: {})
|
|
157
|
-
|
|
158
|
-
build_server(conn_mgr, ctx)
|
|
113
|
+
# @raise [Woods::ConfigurationError] always
|
|
114
|
+
def build(config:) # rubocop:disable Lint/UnusedMethodArgument
|
|
115
|
+
raise Woods::ConfigurationError,
|
|
116
|
+
'The JSON-lines Console bridge is not supported. Use Server.build_embedded, ' \
|
|
117
|
+
'rake woods:console, or Woods::Console::RackMiddleware.'
|
|
159
118
|
end
|
|
160
119
|
|
|
161
120
|
# Build a configured MCP::Server using embedded ActiveRecord execution.
|
|
@@ -170,156 +129,43 @@ module Woods
|
|
|
170
129
|
# {key_column:, value_column:, sensitive_keys: []}. See SafeContext for semantics.
|
|
171
130
|
# @param connection [Object, nil] Database connection for adapter detection
|
|
172
131
|
# @param read_tools_enabled [Boolean] Enable sql/query tools in embedded mode (default: false)
|
|
173
|
-
# @param unsafe_eval_confirmation [Confirmation, nil]
|
|
174
|
-
#
|
|
175
|
-
#
|
|
176
|
-
# @param unsafe_eval_audit_log_path [String, Pathname, nil] JSONL audit log
|
|
177
|
-
# path for `console_eval`. Required when the opt-in is on.
|
|
132
|
+
# @param unsafe_eval_confirmation [Confirmation, nil] Retained for API compatibility;
|
|
133
|
+
# no supported mode registers console_eval.
|
|
134
|
+
# @param unsafe_eval_audit_log_path [String, Pathname, nil] Retained for API compatibility.
|
|
178
135
|
# @return [MCP::Server] Configured server ready for transport
|
|
179
|
-
def build_embedded(model_validator:, safe_context:, redacted_columns: [], # rubocop:disable Metrics/ParameterLists
|
|
136
|
+
def build_embedded(model_validator:, safe_context:, redacted_columns: [], # rubocop:disable Metrics/ParameterLists
|
|
180
137
|
redacted_key_values: [], connection: nil,
|
|
181
138
|
read_tools_enabled: false, model_tables: {},
|
|
182
139
|
model_reflections: {},
|
|
183
140
|
unsafe_eval_confirmation: nil,
|
|
184
141
|
unsafe_eval_audit_log_path: nil)
|
|
185
142
|
require_relative 'embedded_executor'
|
|
186
|
-
enforce_unsafe_eval_contract!
|
|
143
|
+
enforce_unsafe_eval_contract!(
|
|
144
|
+
legacy_options_present: unsafe_eval_confirmation || unsafe_eval_audit_log_path
|
|
145
|
+
)
|
|
187
146
|
|
|
188
|
-
safe_ctx =
|
|
189
|
-
ctx = build_response_context(safe_ctx:
|
|
147
|
+
safe_ctx, render_ctx = policy_contexts(safe_context, redacted_columns, redacted_key_values)
|
|
148
|
+
ctx = build_response_context(safe_ctx: render_ctx, model_tables: model_tables,
|
|
190
149
|
model_reflections: model_reflections)
|
|
191
150
|
|
|
192
|
-
eval_wiring = build_unsafe_eval_wiring(
|
|
193
|
-
confirmation: unsafe_eval_confirmation,
|
|
194
|
-
audit_log_path: unsafe_eval_audit_log_path
|
|
195
|
-
)
|
|
196
|
-
|
|
197
151
|
# Wire the same TableGate into the executor so sql/query are blocked
|
|
198
152
|
# PRE-execution against console_blocked_tables (previously TableGate
|
|
199
153
|
# was only consulted on the render path, leaving the defense inert
|
|
200
154
|
# for the sql and query tools).
|
|
201
|
-
table_gate = ctx&.table_gate
|
|
202
155
|
executor = EmbeddedExecutor.new(
|
|
203
|
-
model_validator: model_validator, safe_context:
|
|
204
|
-
redaction_context: safe_ctx,
|
|
156
|
+
model_validator: model_validator, safe_context: safe_ctx,
|
|
205
157
|
connection: connection, read_tools_enabled: read_tools_enabled,
|
|
206
|
-
table_gate: table_gate
|
|
207
|
-
eval_guard: eval_wiring[:eval_guard],
|
|
208
|
-
confirmation: eval_wiring[:confirmation],
|
|
209
|
-
audit_logger: eval_wiring[:audit_logger],
|
|
210
|
-
unsafe_eval_enabled: eval_wiring[:unsafe_eval_enabled]
|
|
158
|
+
table_gate: ctx&.table_gate
|
|
211
159
|
)
|
|
212
160
|
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
# Resolve the three-collaborator eval wiring for the current config.
|
|
217
|
-
#
|
|
218
|
-
# When `unsafe_eval_enabled?` is false (default), returns nil for all
|
|
219
|
-
# three collaborators — the executor keeps its hard refusal and
|
|
220
|
-
# EvalGuard is never reached.
|
|
221
|
-
#
|
|
222
|
-
# When it's true, BOTH a {Confirmation} and an audit-log path MUST be
|
|
223
|
-
# provided (via kwargs or config); otherwise we raise so a
|
|
224
|
-
# misconfigured host fails at boot instead of silently running Ruby
|
|
225
|
-
# without approval or audit. See backlog B-053.
|
|
226
|
-
#
|
|
227
|
-
# @param confirmation [Confirmation, nil] Explicit kwarg wins over
|
|
228
|
-
# `config.console_unsafe_eval_confirmation`.
|
|
229
|
-
# @param audit_log_path [String, Pathname, nil] Explicit kwarg wins
|
|
230
|
-
# over `config.console_unsafe_eval_audit_log_path`.
|
|
231
|
-
# @return [Hash] { eval_guard:, confirmation:, audit_logger:, unsafe_eval_enabled: }
|
|
232
|
-
# @raise [Woods::ConfigurationError] when opt-in is on but either
|
|
233
|
-
# collaborator is missing.
|
|
234
|
-
def build_unsafe_eval_wiring(confirmation:, audit_log_path:)
|
|
235
|
-
return empty_unsafe_eval_wiring unless unsafe_eval_enabled?
|
|
236
|
-
|
|
237
|
-
config = Woods.configuration if Woods.respond_to?(:configuration)
|
|
238
|
-
confirmation ||= config&.console_unsafe_eval_confirmation
|
|
239
|
-
audit_log_path ||= config&.console_unsafe_eval_audit_log_path
|
|
240
|
-
require_unsafe_eval_collaborators!(confirmation, audit_log_path)
|
|
241
|
-
|
|
242
|
-
{
|
|
243
|
-
eval_guard: EvalGuard.new,
|
|
244
|
-
confirmation: confirmation,
|
|
245
|
-
audit_logger: AuditLogger.new(path: audit_log_path.to_s),
|
|
246
|
-
unsafe_eval_enabled: true
|
|
247
|
-
}
|
|
248
|
-
end
|
|
249
|
-
|
|
250
|
-
def empty_unsafe_eval_wiring
|
|
251
|
-
{ eval_guard: nil, confirmation: nil, audit_logger: nil, unsafe_eval_enabled: false }
|
|
252
|
-
end
|
|
253
|
-
|
|
254
|
-
def require_unsafe_eval_collaborators!(confirmation, audit_log_path)
|
|
255
|
-
if confirmation.nil?
|
|
256
|
-
raise Woods::ConfigurationError,
|
|
257
|
-
'WOODS_CONSOLE_UNSAFE_EVAL is set but no Confirmation was provided. ' \
|
|
258
|
-
'Pass `unsafe_eval_confirmation:` to Server.build_embedded / RackMiddleware ' \
|
|
259
|
-
'or set `config.console_unsafe_eval_confirmation`. Fail-closed by design — ' \
|
|
260
|
-
'see backlog B-053 / docs/CONSOLE_MCP_SETUP.md.'
|
|
261
|
-
end
|
|
262
|
-
return unless audit_log_path.nil? || audit_log_path.to_s.strip.empty?
|
|
263
|
-
|
|
264
|
-
raise Woods::ConfigurationError,
|
|
265
|
-
'WOODS_CONSOLE_UNSAFE_EVAL is set but no audit-log path was provided. ' \
|
|
266
|
-
'Pass `unsafe_eval_audit_log_path:` to Server.build_embedded / RackMiddleware ' \
|
|
267
|
-
'or set `config.console_unsafe_eval_audit_log_path`. Every console_eval run ' \
|
|
268
|
-
'must be audited.'
|
|
269
|
-
end
|
|
270
|
-
|
|
271
|
-
# Register all tool specs for a given tier on the server.
|
|
272
|
-
#
|
|
273
|
-
# @param server [MCP::Server] The MCP server instance
|
|
274
|
-
# @param conn_mgr [ConnectionManager, EmbeddedExecutor] Request executor
|
|
275
|
-
# @param ctx [ResponseContext, nil] Optional context bundling response-safety layers
|
|
276
|
-
# @param tier [Integer] Tier number (1-4)
|
|
277
|
-
# @param renderer [ConsoleResponseRenderer, nil] Optional response renderer
|
|
278
|
-
# @return [void]
|
|
279
|
-
def register_tier_tools(server, conn_mgr, ctx, tier:, renderer: nil)
|
|
280
|
-
TOOL_SPECS.select { |spec| spec.tier == tier }.each do |spec|
|
|
281
|
-
register(spec, server, conn_mgr, ctx, renderer: renderer)
|
|
282
|
-
end
|
|
283
|
-
end
|
|
284
|
-
|
|
285
|
-
# Register Tier 1 read-only tools on the server.
|
|
286
|
-
#
|
|
287
|
-
# @param server [MCP::Server] The MCP server instance
|
|
288
|
-
# @param conn_mgr [ConnectionManager, EmbeddedExecutor] Request executor
|
|
289
|
-
# @param ctx [ResponseContext, nil] Optional context for column redaction
|
|
290
|
-
# @return [void]
|
|
291
|
-
def register_tier1_tools(server, conn_mgr, ctx = nil, renderer: nil)
|
|
292
|
-
register_tier_tools(server, conn_mgr, ctx, tier: 1, renderer: renderer)
|
|
161
|
+
mode = read_tools_enabled ? :embedded_read : :embedded
|
|
162
|
+
build_server(executor, ctx, tool_names: executable_tool_names(mode))
|
|
293
163
|
end
|
|
294
164
|
|
|
295
|
-
#
|
|
296
|
-
#
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
# @param ctx [ResponseContext, nil] Optional context for column redaction
|
|
300
|
-
# @return [void]
|
|
301
|
-
def register_tier2_tools(server, conn_mgr, ctx = nil, renderer: nil)
|
|
302
|
-
register_tier_tools(server, conn_mgr, ctx, tier: 2, renderer: renderer)
|
|
303
|
-
end
|
|
304
|
-
|
|
305
|
-
# Register Tier 3 analytics tools on the server.
|
|
306
|
-
#
|
|
307
|
-
# @param server [MCP::Server] The MCP server instance
|
|
308
|
-
# @param conn_mgr [ConnectionManager, EmbeddedExecutor] Request executor
|
|
309
|
-
# @param ctx [ResponseContext, nil] Optional context for column redaction
|
|
310
|
-
# @return [void]
|
|
311
|
-
def register_tier3_tools(server, conn_mgr, ctx = nil, renderer: nil)
|
|
312
|
-
register_tier_tools(server, conn_mgr, ctx, tier: 3, renderer: renderer)
|
|
313
|
-
end
|
|
314
|
-
|
|
315
|
-
# Register Tier 4 guarded tools on the server.
|
|
316
|
-
#
|
|
317
|
-
# @param server [MCP::Server] The MCP server instance
|
|
318
|
-
# @param conn_mgr [ConnectionManager, EmbeddedExecutor] Request executor
|
|
319
|
-
# @param ctx [ResponseContext, nil] Optional context for column redaction
|
|
320
|
-
# @return [void]
|
|
321
|
-
def register_tier4_tools(server, conn_mgr, ctx = nil, renderer: nil)
|
|
322
|
-
register_tier_tools(server, conn_mgr, ctx, tier: 4, renderer: renderer)
|
|
165
|
+
# Resolve the exact tool list for a supported embedded mode from the
|
|
166
|
+
# same matrix used by tests and documentation evidence.
|
|
167
|
+
def executable_tool_names(mode)
|
|
168
|
+
CONTRACT_MATRIX.filter_map { |row| row[:name] if row[:executable_modes].include?(mode) }
|
|
323
169
|
end
|
|
324
170
|
|
|
325
171
|
private
|
|
@@ -340,7 +186,7 @@ module Woods
|
|
|
340
186
|
pipeline = DispatchPipeline.new(
|
|
341
187
|
tool_name: spec.name,
|
|
342
188
|
handler: spec.handler,
|
|
343
|
-
|
|
189
|
+
properties: spec.properties,
|
|
344
190
|
conn_mgr: conn_mgr,
|
|
345
191
|
ctx: ctx || NullResponseContext.instance,
|
|
346
192
|
renderer: renderer,
|
|
@@ -358,32 +204,72 @@ module Woods
|
|
|
358
204
|
# @param spec [ToolSpec]
|
|
359
205
|
# @return [Hash]
|
|
360
206
|
def spec_schema(spec)
|
|
361
|
-
|
|
362
|
-
schema[:required] = spec.required if spec.required&.any?
|
|
363
|
-
schema
|
|
364
|
-
end
|
|
365
|
-
|
|
366
|
-
# Pre-compute property keys declared as integer in a schema.
|
|
367
|
-
#
|
|
368
|
-
# @param properties [Hash] Tool schema properties
|
|
369
|
-
# @return [Array<Symbol>]
|
|
370
|
-
def integer_property_keys(properties)
|
|
371
|
-
properties.select { |_k, v| v[:type] == 'integer' }.keys.map(&:to_sym)
|
|
207
|
+
spec.input_schema
|
|
372
208
|
end
|
|
373
209
|
|
|
374
|
-
# Build
|
|
375
|
-
#
|
|
210
|
+
# Build the SafeContext (Layer 3) pair the embedded server runs on:
|
|
211
|
+
# [executor context, render context].
|
|
212
|
+
#
|
|
213
|
+
# Audit finding B1: the transport-provided SafeContext owns the
|
|
214
|
+
# connection/pool, the statement timeout, and the rolled-back
|
|
215
|
+
# transaction. Building a second, render-only SafeContext for the
|
|
216
|
+
# redaction lists left the executor's context without the policy, so
|
|
217
|
+
# every executor-side redaction-oracle refusal was dead on the real
|
|
218
|
+
# transports. Whenever redaction is EFFECTIVELY configured — the
|
|
219
|
+
# kwargs when either is supplied, otherwise the lists the supplied
|
|
220
|
+
# context itself carries — derive ONE policy-complete context from
|
|
221
|
+
# the transport context ({SafeContext#with_redaction_policy} — same
|
|
222
|
+
# pool/timeout/transaction behavior, plus the effective lists) and
|
|
223
|
+
# hand that single context to both the executor and the response
|
|
224
|
+
# renderer. The executor refuses oracle shapes; the renderer masks
|
|
225
|
+
# the direct unaliased selections that remain permitted.
|
|
226
|
+
#
|
|
227
|
+
# Fails closed ({Woods::ConfigurationError}) when redaction is
|
|
228
|
+
# effectively configured but the supplied object cannot derive a
|
|
229
|
+
# policy-complete context: the previous silent split wiring left the
|
|
230
|
+
# renderer disabled while the executor still ran the policy. When
|
|
231
|
+
# nothing is effectively configured — no kwargs, and a context that
|
|
232
|
+
# cannot be inspected — the caller's context passes through
|
|
233
|
+
# untouched and the render context stays nil (NullResponseContext).
|
|
234
|
+
#
|
|
235
|
+
# @param safe_context [SafeContext, nil] Transport-provided Layer 3
|
|
376
236
|
# @param redacted_columns [Array<String>]
|
|
377
237
|
# @param redacted_key_values [Array<Hash>]
|
|
378
|
-
# @return [SafeContext, nil]
|
|
379
|
-
def
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
SafeContext.new(
|
|
383
|
-
connection: nil,
|
|
384
|
-
redacted_columns: redacted_columns,
|
|
385
|
-
redacted_key_values: redacted_key_values
|
|
238
|
+
# @return [Array(SafeContext, SafeContext, nil)] executor and render contexts
|
|
239
|
+
def policy_contexts(safe_context, redacted_columns, redacted_key_values)
|
|
240
|
+
configured_columns, configured_key_values = effective_policy(
|
|
241
|
+
safe_context, redacted_columns, redacted_key_values
|
|
386
242
|
)
|
|
243
|
+
return [safe_context, nil] unless configured_columns.any? || configured_key_values.any?
|
|
244
|
+
unless safe_context.respond_to?(:with_redaction_policy)
|
|
245
|
+
raise Woods::ConfigurationError,
|
|
246
|
+
'Console redaction is configured, but the supplied SafeContext cannot derive a ' \
|
|
247
|
+
'policy-complete context (SafeContext#with_redaction_policy). Construction fails ' \
|
|
248
|
+
'closed: the executor would run with the policy while the renderer stays disabled.'
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
derived = safe_context.with_redaction_policy(
|
|
252
|
+
redacted_columns: configured_columns,
|
|
253
|
+
redacted_key_values: configured_key_values
|
|
254
|
+
)
|
|
255
|
+
[derived, derived]
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
# The effective redaction policy: the kwargs when either is supplied
|
|
259
|
+
# (the documented configuration surface), otherwise the lists the
|
|
260
|
+
# supplied context itself carries. Returns empty lists when no kwargs
|
|
261
|
+
# were supplied and the context cannot be inspected (nil, or a
|
|
262
|
+
# duck-typed object without the redacted_* readers) — nothing is
|
|
263
|
+
# effectively configured, so construction proceeds with the caller's
|
|
264
|
+
# context untouched.
|
|
265
|
+
#
|
|
266
|
+
# @return [Array(Array, Array)] [columns, key_values]
|
|
267
|
+
def effective_policy(safe_context, redacted_columns, redacted_key_values)
|
|
268
|
+
return [redacted_columns, redacted_key_values] if redacted_columns.any? || redacted_key_values.any?
|
|
269
|
+
return [[], []] unless safe_context.respond_to?(:redacted_columns) &&
|
|
270
|
+
safe_context.respond_to?(:redacted_key_values)
|
|
271
|
+
|
|
272
|
+
[Array(safe_context.redacted_columns), Array(safe_context.redacted_key_values)]
|
|
387
273
|
end
|
|
388
274
|
|
|
389
275
|
# Bundle the three response-safety layers into a ResponseContext the
|
|
@@ -402,19 +288,19 @@ module Woods
|
|
|
402
288
|
model_reflections: model_reflections)
|
|
403
289
|
end
|
|
404
290
|
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
ResponseContext.build(safe_ctx: safe_ctx, table_gate: table_gate, credential_scanner: scanner)
|
|
408
|
-
end
|
|
409
|
-
|
|
410
|
-
def build_scanner(config)
|
|
291
|
+
secret_index = build_credential_index(config)
|
|
411
292
|
disabled_patterns = Array(config&.console_disabled_scanner_patterns)
|
|
412
|
-
|
|
293
|
+
scanner = if disabled_patterns.include?(:all)
|
|
294
|
+
nil
|
|
295
|
+
else
|
|
296
|
+
CredentialScanner.new(
|
|
297
|
+
disabled_patterns: disabled_patterns,
|
|
298
|
+
secret_index: secret_index
|
|
299
|
+
)
|
|
300
|
+
end
|
|
301
|
+
@active_scanner = scanner
|
|
413
302
|
|
|
414
|
-
|
|
415
|
-
CredentialScanner.new(disabled_patterns: disabled_patterns,
|
|
416
|
-
secret_index: build_credential_index(config))
|
|
417
|
-
end
|
|
303
|
+
ResponseContext.build(safe_ctx: safe_ctx, table_gate: table_gate, credential_scanner: scanner)
|
|
418
304
|
end
|
|
419
305
|
|
|
420
306
|
# Build the boot-time credential index from Rails.application's encrypted
|
|
@@ -469,40 +355,23 @@ module Woods
|
|
|
469
355
|
|
|
470
356
|
# Shared server construction used by both build() and build_embedded().
|
|
471
357
|
#
|
|
472
|
-
# @param conn_mgr [
|
|
358
|
+
# @param conn_mgr [EmbeddedExecutor] Request executor
|
|
473
359
|
# @param ctx [ResponseContext, nil] Optional context bundling response-safety layers
|
|
360
|
+
# @param tool_names [Array<String>] Exact executable tool names to advertise
|
|
474
361
|
# @return [MCP::Server]
|
|
475
|
-
def build_server(conn_mgr, ctx)
|
|
362
|
+
def build_server(conn_mgr, ctx, tool_names:)
|
|
476
363
|
server = ::MCP::Server.new(
|
|
477
364
|
name: 'woods-console',
|
|
478
|
-
version: defined?(Woods::VERSION) ? Woods::VERSION : '0.1.0'
|
|
365
|
+
version: defined?(Woods::VERSION) ? Woods::VERSION : '0.1.0',
|
|
366
|
+
**Woods::MCP::ProtocolPolicy.cache_hints
|
|
479
367
|
)
|
|
480
368
|
|
|
481
369
|
renderer = build_console_renderer
|
|
482
370
|
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
server
|
|
488
|
-
end
|
|
489
|
-
|
|
490
|
-
# Loud multi-line banner surfaced to stderr when the opt-in flag is
|
|
491
|
-
# recognised outside of production. Operators should see this every
|
|
492
|
-
# boot so an accidentally-persistent env var cannot go unnoticed.
|
|
493
|
-
#
|
|
494
|
-
# @return [String]
|
|
495
|
-
def unsafe_eval_banner
|
|
496
|
-
<<~BANNER
|
|
497
|
-
|
|
498
|
-
================================================================================
|
|
499
|
-
WOODS_CONSOLE_UNSAFE_EVAL IS SET
|
|
500
|
-
console_eval is LIVE on this process. Every run goes through EvalGuard +
|
|
501
|
-
Confirmation + SafeContext rollback + a wall-clock timeout and is recorded
|
|
502
|
-
to the audit log. The flag refuses to boot in Rails.env.production?.
|
|
503
|
-
If you did not mean to set this, unset the env var.
|
|
504
|
-
================================================================================
|
|
505
|
-
BANNER
|
|
371
|
+
TOOL_SPECS.select { |spec| tool_names.include?(spec.name) }.each do |spec|
|
|
372
|
+
register(spec, server, conn_mgr, ctx, renderer: renderer)
|
|
373
|
+
end
|
|
374
|
+
Woods::MCP::ProtocolPolicy.sort_tools!(server)
|
|
506
375
|
end
|
|
507
376
|
|
|
508
377
|
def structured_logger
|