Class: Woods::Console::EmbeddedExecutor

Inherits:
Object
  • Object
show all
Defined in:
lib/woods/console/embedded_executor.rb

Overview

Drop-in replacement for ConnectionManager + the bridge process that executes queries directly via ActiveRecord instead of going over the JSON-lines protocol (see StubBridge for the protocol scaffold).

Implements the same ‘send_request(Hash) -> Hash` interface as ConnectionManager, so all existing tool definitions in Server work unchanged — just pass this where `conn_mgr` goes.

Examples:

executor = EmbeddedExecutor.new(model_validator: validator, safe_context: ctx)
response = executor.send_request({ 'tool' => 'count', 'params' => { 'model' => 'User' } })
# => { 'ok' => true, 'result' => { 'count' => 42 }, 'timing_ms' => 1.2 }

Constant Summary collapse

AGGREGATE_FUNCTIONS =

rubocop:disable Metrics/ClassLength

%w[sum average minimum maximum count].freeze
TIER1_TOOLS =
BridgeProtocol::TIER1_TOOLS
EMBEDDED_READ_TOOLS =

Tools gated behind the read_tools_enabled flag. sql/query have existing safety gates (SqlValidator, SafeContext rollback) but require explicit opt-in for embedded mode.

%w[sql query].freeze
MAX_SQL_LIMIT =
10_000
MAX_QUERY_LIMIT =
10_000
MIN_EVAL_TIMEOUT =
1
MAX_EVAL_TIMEOUT =
30
DEFAULT_EVAL_TIMEOUT =
10

Instance Method Summary collapse

Constructor Details

#initialize(model_validator:, safe_context:, connection: nil, read_tools_enabled: false, table_gate: nil, eval_guard: nil, confirmation: nil, audit_logger: nil, unsafe_eval_enabled: false) ⇒ EmbeddedExecutor

Returns a new instance of EmbeddedExecutor.

Parameters:

  • model_validator (ModelValidator)

    Validates model/column names

  • safe_context (SafeContext)

    Wraps execution in rolled-back transaction

  • connection (Object, nil) (defaults to: nil)

    Database connection for adapter detection

  • read_tools_enabled (Boolean) (defaults to: false)

    Enable sql/query tools in embedded mode (default: false)

  • table_gate (TableGate, nil) (defaults to: nil)

    Enforces console_blocked_tables on every model/join/association/SQL access pre-execution. When nil, no table-level gate runs — an explicit signal from the server builder that no tables are configured as blocked. Callers should pass the live gate from Server.build_response_context so embedded mode matches the bridge’s defense-in-depth posture.

  • eval_guard (#check!, nil) (defaults to: nil)

    EvalGuard for the ‘console_eval` opt-in path. Required when `unsafe_eval_enabled` is true; nil otherwise.

  • confirmation (Confirmation, nil) (defaults to: nil)

    Human-in-the-loop approval for ‘console_eval`. Required when `unsafe_eval_enabled` is true; nil otherwise.

  • audit_logger (AuditLogger, nil) (defaults to: nil)

    Logs every ‘console_eval` attempt (refused, denied, or executed). Required when `unsafe_eval_enabled` is true; nil otherwise.

  • unsafe_eval_enabled (Boolean) (defaults to: false)

    When true, the executor wires the five-control eval path (guard + confirmation + SafeContext + timeout + audit). When false, ‘console_eval` returns the hard `eval_disabled` refusal as before.



69
70
71
72
73
74
75
76
77
78
79
80
81
# File 'lib/woods/console/embedded_executor.rb', line 69

def initialize(model_validator:, safe_context:, connection: nil, read_tools_enabled: false, # rubocop:disable Metrics/ParameterLists
               table_gate: nil, eval_guard: nil, confirmation: nil, audit_logger: nil,
               unsafe_eval_enabled: false)
  @model_validator = model_validator
  @safe_context = safe_context
  @connection = connection
  @read_tools_enabled = read_tools_enabled
  @table_gate = table_gate
  @eval_guard = eval_guard
  @confirmation = confirmation
  @audit_logger = audit_logger
  @unsafe_eval_enabled = unsafe_eval_enabled
end

Instance Method Details

#send_request(request) ⇒ Hash

Execute a tool request and return a response hash.

Compatible with ConnectionManager#send_request — Server’s ‘send_to_bridge` calls this method and expects `{ ’ok’ => true/false, … }‘.

Parameters:

  • request (Hash)

    Request with ‘tool’ and ‘params’ keys

Returns:

  • (Hash)

    Response with ‘ok’, ‘result’/‘error’, and ‘timing_ms’



90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
# File 'lib/woods/console/embedded_executor.rb', line 90

def send_request(request)
  # Deep-stringify keys — Tier1 tool builders use symbol keys, but the bridge
  # path naturally stringifies via JSON round-trip. Replicate that here.
  request = deep_stringify_keys(request)
  tool = request['tool']
  params = request['params'] || {}

  refusal = refusal_for(tool)
  return refusal if refusal

  start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
  result = @safe_context.execute { dispatch(tool, params) }
  elapsed = ((Process.clock_gettime(Process::CLOCK_MONOTONIC) - start_time) * 1000).round(1)

  { 'ok' => true, 'result' => result, 'timing_ms' => elapsed }
rescue ValidationError => e
  # Validation messages are author-controlled — safe to return as-is so
  # callers can correct their request.
  { 'ok' => false, 'error' => e.message, 'error_type' => 'validation' }
rescue StandardError => e
  # Execution errors come from adapters and can embed fragments of the
  # rejected SQL, schema names, column names, or partial table contents
  # (`PG::UndefinedColumn`, `Mysql2::Error`, etc.). Return a generic
  # reason to the client; log the full detail via Rails.logger when
  # available so operators can still debug.
  log_execution_error(e)
  { 'ok' => false, 'error' => sanitize_execution_error(e), 'error_type' => 'execution' }
end