Class: RubynCode::IDE::Adapters::ToolOutput

Inherits:
Object
  • Object
show all
Defined in:
lib/rubyn_code/ide/adapters/tool_output.rb

Overview

Wraps every tool invocation in IDE mode. Emits JSON-RPC notifications that the VS Code extension consumes, precomputes file edits so the editor can render a diff before any write touches disk, and gates mutating operations behind acceptance/approval from the IDE client.

Gating policy depends on the permission mode:

:default      → approve every mutating tool + every file edit
:accept_edits → auto-approve file edits, prompt for bash/other
:plan_only    → read-only, block all writes
:auto         → auto-approve everything except deny-listed
:dont_ask     → auto-deny all non-read-only tools
:bypass       → no checks (legacy yolo)

In all modes the adapter emits notifications so the UI reflects what’s happening.

Constant Summary collapse

WAIT_POLL_INTERVAL =

Wake up periodically to check for thread interrupts (cancel). No auto-deny — waits indefinitely until the user decides.

5
READ_ONLY_TOOLS =

seconds

%w[
  read_file glob grep
  git_status git_diff git_log git_commit
  memory_search web_fetch web_search
  run_specs
].freeze
FILE_WRITE_TOOLS =
%w[write_file edit_file].freeze
VALID_PERMISSION_MODES =
%i[default accept_edits plan_only auto dont_ask bypass].freeze

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(server, permission_mode: :default, yolo: false, hook_runner: nil) ⇒ ToolOutput

Returns a new instance of ToolOutput.



41
42
43
44
45
46
47
48
49
50
51
52
# File 'lib/rubyn_code/ide/adapters/tool_output.rb', line 41

def initialize(server, permission_mode: :default, yolo: false, hook_runner: nil)
  @server          = server
  @permission_mode = yolo ? :bypass : permission_mode.to_sym
  @hook_runner     = hook_runner
  @mutex           = Mutex.new

  # { request_id => { cv: ConditionVariable, approved: nil|true|false } }
  @pending_approvals = {}

  # { edit_id => { cv: ConditionVariable, accepted: nil|true|false } }
  @pending_edits = {}
end

Instance Attribute Details

#permission_modeObject

Returns the value of attribute permission_mode.



39
40
41
# File 'lib/rubyn_code/ide/adapters/tool_output.rb', line 39

def permission_mode
  @permission_mode
end

Instance Method Details

#resolve_approval(request_id, approved) ⇒ Object

Called by ApproveToolUseHandler when the IDE client responds.



98
99
100
101
102
103
104
105
106
107
# File 'lib/rubyn_code/ide/adapters/tool_output.rb', line 98

def resolve_approval(request_id, approved)
  @mutex.synchronize do
    pending = @pending_approvals[request_id]
    return false unless pending

    pending[:approved] = approved
    pending[:cv].signal
    true
  end
end

#resolve_edit(edit_id, accepted) ⇒ Object

Called by AcceptEditHandler when the IDE client responds.



110
111
112
113
114
115
116
117
118
119
# File 'lib/rubyn_code/ide/adapters/tool_output.rb', line 110

def resolve_edit(edit_id, accepted)
  @mutex.synchronize do
    pending = @pending_edits[edit_id]
    return false unless pending

    pending[:accepted] = accepted
    pending[:cv].signal
    true
  end
end

#wrap_execution(tool_name, args, &block) ⇒ Object

Main entry point. Wraps a tool call, emitting IDE notifications and gating execution behind acceptance when required.

adapter.wrap_execution("write_file", { path: "foo.rb", content: "..." }) do
  executor.execute("write_file", params)
end


61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
# File 'lib/rubyn_code/ide/adapters/tool_output.rb', line 61

def wrap_execution(tool_name, args, &block)
  request_id = generate_id
  args = stringify_keys(args)

  return execute_and_notify(request_id, tool_name, args, &block) if read_only?(tool_name)

  # Non-read-only tools: gating depends on permission mode
  case @permission_mode
  when :bypass, :auto
    # Auto-approve everything — run without waiting
    execute_and_notify(request_id, tool_name, args, &block)
  when :plan_only
    emit_tool_use(request_id, tool_name, args, requires_approval: false)
    msg = 'Plan mode: write operations blocked'
    emit_tool_result(request_id, tool_name, msg, success: false, args: args)
    raise RubynCode::UserDeniedError, msg
  when :dont_ask
    emit_tool_use(request_id, tool_name, args, requires_approval: false)
    msg = 'Auto-denied: permission mode is dont_ask'
    emit_tool_result(request_id, tool_name, msg, success: false, args: args)
    raise RubynCode::UserDeniedError, msg
  when :accept_edits
    if file_write?(tool_name)
      execute_with_edit_gate(request_id, tool_name, args, &block)
    else
      execute_with_approval(request_id, tool_name, args, &block)
    end
  else # :default
    if file_write?(tool_name)
      execute_with_edit_gate(request_id, tool_name, args, &block)
    else
      execute_with_approval(request_id, tool_name, args, &block)
    end
  end
end