woods 1.6.1 → 2.0.0.beta2
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 +2035 -0
- data/CONTRIBUTING.md +253 -87
- data/README.md +161 -513
- 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 +655 -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 +455 -0
- data/docs/INTERNALS.md +418 -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 +213 -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 +40 -4
- data/exe/woods-console-mcp +21 -35
- data/exe/woods-mcp +20 -7
- data/exe/woods-mcp-http +80 -11
- 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 +171 -2
- data/lib/woods/builder.rb +310 -22
- data/lib/woods/cache/cache_middleware.rb +7 -2
- 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/dispatch_pipeline.rb +42 -12
- data/lib/woods/console/embedded_executor.rb +806 -149
- 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 +29 -1
- data/lib/woods/console/rack_middleware.rb +65 -42
- data/lib/woods/console/redactor.rb +26 -8
- data/lib/woods/console/safe_context.rb +58 -10
- data/lib/woods/console/scope_predicate_parser.rb +41 -0
- data/lib/woods/console/server.rb +119 -247
- data/lib/woods/console/sql_noise_stripper.rb +125 -16
- data/lib/woods/console/sql_table_scanner.rb +82 -22
- data/lib/woods/console/sql_validator.rb +459 -29
- data/lib/woods/console/table_gate.rb +2 -2
- data/lib/woods/console/tool_specs.rb +463 -90
- data/lib/woods/console/tools/tier1.rb +1 -5
- data/lib/woods/console/tools/tier4.rb +18 -9
- 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 +2783 -238
- 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 +147 -26
- data/lib/woods/flow_document.rb +1 -0
- data/lib/woods/flow_precomputer.rb +175 -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 +564 -87
- data/lib/woods/index_artifact.rb +93 -23
- data/lib/woods/mcp/bearer_auth.rb +102 -13
- 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 +66 -7
- 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 +898 -152
- 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/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 +236 -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 +69 -30
- 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 +26 -8
- 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/trace_enricher.rb +3 -0
- data/lib/woods/ruby_analyzer.rb +21 -5
- data/lib/woods/session_tracer/file_store.rb +138 -19
- data/lib/woods/session_tracer/middleware.rb +1 -2
- data/lib/woods/session_tracer/redis_store.rb +122 -12
- data/lib/woods/session_tracer/session_flow_assembler.rb +57 -17
- data/lib/woods/session_tracer/session_flow_document.rb +56 -14
- 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 +138 -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 +134 -23
- 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/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/notion/mapper.rb +0 -40
- data/lib/woods/observability/health_check.rb +0 -79
- data/lib/woods/observability/instrumentation.rb +0 -34
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require 'json'
|
|
4
|
-
require 'open3'
|
|
5
3
|
require 'shellwords'
|
|
6
4
|
|
|
7
5
|
# @see Woods
|
|
@@ -11,162 +9,79 @@ module Woods
|
|
|
11
9
|
module Console
|
|
12
10
|
class ConnectionError < Woods::Error; end
|
|
13
11
|
|
|
14
|
-
#
|
|
15
|
-
#
|
|
16
|
-
# Spawns and manages the bridge process, sends JSON-lines requests,
|
|
17
|
-
# receives responses. Implements heartbeat (30s) and reconnect with
|
|
18
|
-
# exponential backoff (max 5 retries).
|
|
19
|
-
#
|
|
20
|
-
# @example
|
|
21
|
-
# manager = ConnectionManager.new(config: {
|
|
22
|
-
# 'mode' => 'direct',
|
|
23
|
-
# 'command' => 'bundle exec rails runner bridge.rb'
|
|
24
|
-
# })
|
|
25
|
-
# manager.connect!
|
|
26
|
-
# response = manager.send_request({ 'id' => 'r1', 'tool' => 'status', 'params' => {} })
|
|
27
|
-
# manager.disconnect!
|
|
12
|
+
# Resolves and launches an embedded Console MCP process.
|
|
28
13
|
#
|
|
14
|
+
# The former implementation spoke a private JSON-lines bridge protocol to
|
|
15
|
+
# a scaffold that returned static data. The supported path now forwards the
|
|
16
|
+
# MCP client's stdio directly to the real embedded server by replacing this
|
|
17
|
+
# process with a direct, Docker, or SSH command.
|
|
29
18
|
class ConnectionManager
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
# @
|
|
34
|
-
# @option config [String] '
|
|
35
|
-
# @option config [String] '
|
|
36
|
-
# @option config [String] 'container'
|
|
37
|
-
# @option config [String] '
|
|
38
|
-
# @option config [String] '
|
|
39
|
-
# @option config [String] 'user' SSH user (ssh mode)
|
|
19
|
+
DEFAULT_COMMAND = 'bundle exec rake woods:console'
|
|
20
|
+
|
|
21
|
+
# @param config [Hash] Process-launch configuration
|
|
22
|
+
# @option config [String] 'mode' direct, docker, or ssh (default: direct)
|
|
23
|
+
# @option config [String] 'command' Embedded server command
|
|
24
|
+
# @option config [String] 'directory' Working directory for direct mode
|
|
25
|
+
# @option config [String] 'container' Container name for docker mode
|
|
26
|
+
# @option config [String] 'host' SSH host for ssh mode
|
|
27
|
+
# @option config [String] 'user' Optional SSH user
|
|
40
28
|
def initialize(config:)
|
|
41
29
|
@config = config
|
|
42
|
-
@mode = config
|
|
43
|
-
@
|
|
44
|
-
@stdin = nil
|
|
45
|
-
@stdout = nil
|
|
46
|
-
@wait_thread = nil
|
|
47
|
-
@retries = 0
|
|
48
|
-
@last_heartbeat = nil
|
|
30
|
+
@mode = config.fetch('mode', 'direct')
|
|
31
|
+
@embedded_command = config.fetch('command', DEFAULT_COMMAND)
|
|
49
32
|
end
|
|
50
33
|
|
|
51
|
-
#
|
|
34
|
+
# Return the argv used to launch the embedded MCP server.
|
|
52
35
|
#
|
|
53
|
-
# @return [
|
|
54
|
-
# @raise [ConnectionError]
|
|
55
|
-
def
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
else
|
|
62
|
-
@stdin, @stdout, @wait_thread = Open3.popen2(*cmd)
|
|
36
|
+
# @return [Array<String>]
|
|
37
|
+
# @raise [ConnectionError] when the mode is invalid or incomplete
|
|
38
|
+
def command
|
|
39
|
+
case @mode
|
|
40
|
+
when 'direct' then embedded_argv
|
|
41
|
+
when 'docker' then docker_command
|
|
42
|
+
when 'ssh' then ssh_command
|
|
43
|
+
else raise ConnectionError, "Unknown connection mode: #{@mode}"
|
|
63
44
|
end
|
|
64
|
-
@last_heartbeat = Time.now
|
|
65
|
-
@retries = 0
|
|
66
|
-
rescue StandardError => e
|
|
67
|
-
raise ConnectionError, "Failed to connect (#{@mode}): #{e.message}"
|
|
68
45
|
end
|
|
69
46
|
|
|
70
|
-
#
|
|
47
|
+
# Replace the current process with the embedded MCP server.
|
|
71
48
|
#
|
|
72
|
-
#
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
@stdout&.close
|
|
76
|
-
@wait_thread&.value
|
|
77
|
-
@stdin = nil
|
|
78
|
-
@stdout = nil
|
|
79
|
-
@wait_thread = nil
|
|
80
|
-
end
|
|
81
|
-
|
|
82
|
-
# Send a request to the bridge and read the response.
|
|
83
|
-
#
|
|
84
|
-
# @param request [Hash] JSON-serializable request hash
|
|
85
|
-
# @return [Hash] Parsed response hash
|
|
86
|
-
# @raise [ConnectionError] if communication fails after retries
|
|
87
|
-
def send_request(request)
|
|
88
|
-
ensure_connected!
|
|
89
|
-
@stdin.puts(JSON.generate(request))
|
|
90
|
-
@stdin.flush
|
|
91
|
-
line = @stdout.gets
|
|
92
|
-
raise ConnectionError, 'Bridge process closed unexpectedly' unless line
|
|
93
|
-
|
|
94
|
-
@last_heartbeat = Time.now
|
|
95
|
-
JSON.parse(line)
|
|
96
|
-
rescue IOError, Errno::EPIPE, Errno::ECONNRESET => e
|
|
97
|
-
reconnect_or_raise!(e)
|
|
98
|
-
retry
|
|
99
|
-
end
|
|
100
|
-
|
|
101
|
-
# Check if the bridge process is alive.
|
|
49
|
+
# Process replacement gives the MCP client direct ownership of lifecycle:
|
|
50
|
+
# EOF, INT, and TERM reach the real server without an intermediate process
|
|
51
|
+
# that can hang while waiting for a child.
|
|
102
52
|
#
|
|
103
|
-
# @return [
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
def heartbeat_needed?
|
|
114
|
-
return false unless @last_heartbeat
|
|
115
|
-
|
|
116
|
-
(Time.now - @last_heartbeat) >= HEARTBEAT_INTERVAL
|
|
53
|
+
# @return [void]
|
|
54
|
+
# @raise [ConnectionError] when the process cannot be launched
|
|
55
|
+
def replace_process!
|
|
56
|
+
if @mode == 'direct' && @config['directory']
|
|
57
|
+
Dir.chdir(@config['directory']) { exec(*command) }
|
|
58
|
+
else
|
|
59
|
+
exec(*command)
|
|
60
|
+
end
|
|
61
|
+
rescue SystemCallError, ArgumentError => e
|
|
62
|
+
raise ConnectionError, "Failed to launch embedded Console MCP (#{@mode}): #{e.message}"
|
|
117
63
|
end
|
|
118
64
|
|
|
119
65
|
private
|
|
120
66
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
when 'direct' then build_direct_command
|
|
129
|
-
else raise ConnectionError, "Unknown connection mode: #{@mode}"
|
|
130
|
-
end
|
|
67
|
+
def embedded_argv
|
|
68
|
+
argv = @embedded_command.to_s.shellsplit
|
|
69
|
+
raise ConnectionError, 'Console command must not be empty' if argv.empty?
|
|
70
|
+
|
|
71
|
+
argv
|
|
72
|
+
rescue ArgumentError => e
|
|
73
|
+
raise ConnectionError, "Invalid console command: #{e.message}"
|
|
131
74
|
end
|
|
132
75
|
|
|
133
|
-
def
|
|
76
|
+
def docker_command
|
|
134
77
|
container = @config['container'] || raise(ConnectionError, 'Docker mode requires container name')
|
|
135
|
-
['docker', 'exec', '-i', container] +
|
|
78
|
+
['docker', 'exec', '-i', container] + embedded_argv
|
|
136
79
|
end
|
|
137
80
|
|
|
138
|
-
def
|
|
81
|
+
def ssh_command
|
|
139
82
|
host = @config['host'] || raise(ConnectionError, 'SSH mode requires host')
|
|
140
|
-
|
|
141
|
-
target
|
|
142
|
-
['ssh', target] + @command.shellsplit
|
|
143
|
-
end
|
|
144
|
-
|
|
145
|
-
def build_direct_command
|
|
146
|
-
@command.shellsplit
|
|
147
|
-
end
|
|
148
|
-
|
|
149
|
-
# Ensure the connection is active.
|
|
150
|
-
def ensure_connected!
|
|
151
|
-
return if alive?
|
|
152
|
-
|
|
153
|
-
connect!
|
|
154
|
-
end
|
|
155
|
-
|
|
156
|
-
# Attempt reconnection with exponential backoff.
|
|
157
|
-
#
|
|
158
|
-
# @param error [StandardError] The original error
|
|
159
|
-
# @raise [ConnectionError] if max retries exceeded
|
|
160
|
-
def reconnect_or_raise!(error)
|
|
161
|
-
@retries += 1
|
|
162
|
-
if @retries > MAX_RETRIES
|
|
163
|
-
raise ConnectionError,
|
|
164
|
-
"Connection failed after #{MAX_RETRIES} retries: #{error.message}"
|
|
165
|
-
end
|
|
166
|
-
|
|
167
|
-
sleep((2**(@retries - 1)) * 0.1)
|
|
168
|
-
disconnect!
|
|
169
|
-
connect!
|
|
83
|
+
target = @config['user'] ? "#{@config['user']}@#{host}" : host
|
|
84
|
+
['ssh', target] + embedded_argv
|
|
170
85
|
end
|
|
171
86
|
end
|
|
172
87
|
end
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require 'json'
|
|
4
4
|
require_relative 'response_context'
|
|
5
|
+
require_relative 'input_contract'
|
|
5
6
|
|
|
6
7
|
module Woods
|
|
7
8
|
module Console
|
|
@@ -10,8 +11,7 @@ module Woods
|
|
|
10
11
|
# Encapsulates everything that happens between MCP's `define_tool` block
|
|
11
12
|
# firing and the `MCP::Tool::Response` returning to the client:
|
|
12
13
|
#
|
|
13
|
-
# 1.
|
|
14
|
-
# `"10"` for an `integer` property — we normalize before dispatch.
|
|
14
|
+
# 1. Strictly parse integer strings and enforce schema bounds.
|
|
15
15
|
# 2. Run the Layer 1 table gate ({ResponseContext#enforce!}).
|
|
16
16
|
# 3. Translate the tool args into a bridge/executor request via the
|
|
17
17
|
# handler proc supplied in the {ToolSpec}.
|
|
@@ -31,8 +31,7 @@ module Woods
|
|
|
31
31
|
class DispatchPipeline
|
|
32
32
|
# @param tool_name [String]
|
|
33
33
|
# @param handler [#call] Maps a Hash of tool args to a bridge request Hash.
|
|
34
|
-
# @param
|
|
35
|
-
# in the tool schema.
|
|
34
|
+
# @param properties [Hash] ToolSpec JSON Schema properties.
|
|
36
35
|
# @param conn_mgr [#send_request] ConnectionManager or EmbeddedExecutor.
|
|
37
36
|
# @param ctx [ResponseContext] Bundles the three response-safety layers.
|
|
38
37
|
# Defaults to the Null context so tests can stub minimally.
|
|
@@ -40,11 +39,11 @@ module Woods
|
|
|
40
39
|
# @param logger [#warn, nil] Structured logger used for credential-scan
|
|
41
40
|
# and table-gate telemetry. Failures are swallowed so observability
|
|
42
41
|
# issues never break a tool response.
|
|
43
|
-
def initialize(tool_name:, handler:,
|
|
42
|
+
def initialize(tool_name:, handler:, properties:, conn_mgr:, # rubocop:disable Metrics/ParameterLists
|
|
44
43
|
ctx: NullResponseContext.instance, renderer: nil, logger: nil)
|
|
45
44
|
@tool_name = tool_name
|
|
46
45
|
@handler = handler
|
|
47
|
-
@
|
|
46
|
+
@properties = properties
|
|
48
47
|
@conn_mgr = conn_mgr
|
|
49
48
|
@ctx = ctx
|
|
50
49
|
@renderer = renderer
|
|
@@ -56,21 +55,48 @@ module Woods
|
|
|
56
55
|
# @param args [Hash] Tool arguments (symbol keys from MCP).
|
|
57
56
|
# @return [MCP::Tool::Response]
|
|
58
57
|
def call(args)
|
|
59
|
-
|
|
58
|
+
InputContract.normalize!(args, @properties)
|
|
60
59
|
@ctx.enforce!(args)
|
|
61
60
|
request = @handler.call(args).transform_keys(&:to_s)
|
|
62
61
|
send_to_bridge(request)
|
|
63
62
|
rescue TableGateError => e
|
|
64
63
|
log_table_gate_rejection(args, e)
|
|
65
|
-
error_response(e.message)
|
|
66
|
-
rescue SqlValidationError, ForbiddenExpressionError => e
|
|
67
|
-
error_response(e.message)
|
|
64
|
+
error_response(scan_early_error(e.message))
|
|
65
|
+
rescue InputContract::ValidationError, SqlValidationError, ForbiddenExpressionError => e
|
|
66
|
+
error_response(scan_early_error(e.message))
|
|
67
|
+
rescue StandardError => e
|
|
68
|
+
# Catch-all for pipeline bugs — a renderer NoMethodError, an encoding
|
|
69
|
+
# oddity, a handler defect (L9). Without it the exception escaped into
|
|
70
|
+
# the `mcp` gem's own handling of a raising tool block, which can echo
|
|
71
|
+
# `Class: message` to the client: a leak channel the executor's
|
|
72
|
+
# responses are already sanitized against. Answer with the same
|
|
73
|
+
# sanitized shape the executor uses (class name only, details logged
|
|
74
|
+
# server-side), still routed through the credential scan so nothing an
|
|
75
|
+
# exception message picked up reaches the client either.
|
|
76
|
+
log_unexpected_error(e)
|
|
77
|
+
error_response(scan_early_error(sanitized_unexpected_error(e)))
|
|
68
78
|
end
|
|
69
79
|
|
|
70
80
|
private
|
|
71
81
|
|
|
72
|
-
|
|
73
|
-
|
|
82
|
+
# Client-safe stand-in for an unexpected exception. Names the tool so an
|
|
83
|
+
# agent can retry or report, and nothing else — the class and message
|
|
84
|
+
# go to the log instead.
|
|
85
|
+
def sanitized_unexpected_error(_error)
|
|
86
|
+
"Internal error in #{@tool_name} (details logged server-side)"
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def log_unexpected_error(error)
|
|
90
|
+
return unless @logger
|
|
91
|
+
|
|
92
|
+
@logger.warn(
|
|
93
|
+
'console.dispatch.unexpected_error',
|
|
94
|
+
tool: @tool_name,
|
|
95
|
+
error_class: error.class.name,
|
|
96
|
+
message: error.message
|
|
97
|
+
)
|
|
98
|
+
rescue StandardError
|
|
99
|
+
nil
|
|
74
100
|
end
|
|
75
101
|
|
|
76
102
|
def send_to_bridge(request)
|
|
@@ -98,6 +124,10 @@ module Woods
|
|
|
98
124
|
scanned
|
|
99
125
|
end
|
|
100
126
|
|
|
127
|
+
def scan_early_error(message)
|
|
128
|
+
scan_for_credentials(message, 'tool' => @tool_name.delete_prefix('console_'))
|
|
129
|
+
end
|
|
130
|
+
|
|
101
131
|
def log_credential_hits(request, counts)
|
|
102
132
|
return unless @logger
|
|
103
133
|
|