Class: Rubino::Tools::Base

Inherits:
Object
  • Object
show all
Defined in:
lib/rubino/tools/base.rb

Overview

Abstract base class for all tools. Each tool must implement: name, description, input_schema, risk_level, call.

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Instance Attribute Details

#cancel_tokenObject

Set by ToolExecutor before each call so long-running tools (shell, http, watchers) can poll for user cancellation. Default is nil — the tool should treat that as “no cancellation possible” and not crash.



11
12
13
# File 'lib/rubino/tools/base.rb', line 11

def cancel_token
  @cancel_token
end

#read_trackerObject

Session-scoped ReadTracker injected by ToolExecutor. ReadTool registers successful reads; EditTool / MultiEditTool consult it before writing so they can refuse to edit a file the model never opened in this session. Nil-tolerant: tools that don’t care just ignore it.



18
19
20
# File 'lib/rubino/tools/base.rb', line 18

def read_tracker
  @read_tracker
end

#stream_chunkObject

Optional Proc, injected by ToolExecutor, that the tool can call with incremental output chunks during a long-running call. ShellTool uses this to stream stdout/stderr lines as the subprocess writes them instead of dumping everything at end-of-command. Nil-tolerant: a tool with no streamable output (read, edit, glob) just ignores it.



25
26
27
# File 'lib/rubino/tools/base.rb', line 25

def stream_chunk
  @stream_chunk
end

#stream_kindObject

Optional render hint the ToolExecutor forwards to the UI alongside each streamed chunk (and the end-of-call body). :diff makes the CLI colorize +/-/@@ lines AND show the full hunks instead of collapsing to the 3-line preview — so “show me the diff” surfaces the real diff, not a snippet. Default nil ⇒ :plain. Set it from #call once the command/content kind is known; the streaming lambda reads it live.



33
34
35
# File 'lib/rubino/tools/base.rb', line 33

def stream_kind
  @stream_kind
end

Class Method Details

.workspace_rootObject

Filesystem sandbox for write/edit/delete operations.

Defaults to Dir.pwd, overridable via terminal.cwd in config. Mutating tools must call within_workspace? before touching the disk so a prompt injection that asks for ‘file_path: “/etc/passwd”` is refused at the tool boundary, before the approval prompt even sees the path.

The check resolves every symlink with File.realpath before comparing against the workspace root: dropping a ‘link → /etc` inside the workspace and writing through it used to bypass the boundary because expand_path alone never crosses the symlink. realpath walks the filesystem and gives us the canonical destination, so an in-workspace path that ultimately points outside is rejected like any other escape. For non-existent targets (write-creates-new-file) we resolve the deepest existing ancestor and re-attach the remainder — the new file will land at that ancestor, so the ancestor is what we sandbox.

Set tools.workspace_strict=false in config.yml to disable globally (the agent then trusts the model + the approval flow alone). The directory tools sandbox to. Exposed as a class method so the File API operations can root their Workspace at the SAME place (otherwise produced artifacts under this root look like traversal escapes relative to paths_home and the download 422s). The PRIMARY root — terminal.cwd or the launch cwd. Kept as the single source of truth for “the” directory: the @-picker, shell/test cwd, the File API workspace and the attachment downloader all root here so they agree. The write/edit SANDBOX, however, spans every root (see #within_workspace?) so an added dir is also writable.



149
150
151
# File 'lib/rubino/tools/base.rb', line 149

def self.workspace_root
  Workspace.primary_root
end

.workspace_rootsObject

Every allowed root (primary + any –add-dir / /add-dir dirs). The sandbox accepts a target under ANY of these.



155
156
157
# File 'lib/rubino/tools/base.rb', line 155

def self.workspace_roots
  Workspace.roots
end

Instance Method Details

#call(arguments) ⇒ Object

Executes the tool with given arguments, returns output string

Raises:

  • (NotImplementedError)


81
82
83
# File 'lib/rubino/tools/base.rb', line 81

def call(arguments)
  raise NotImplementedError, "#{self.class}#call not implemented"
end

#cancellation_requested?Boolean

True when the user has requested cancellation. Cheap, lock-protected. Use in tight loops; on true, terminate gracefully and either return an “interrupted” string or raise Rubino::Interrupted.

Returns:

  • (Boolean)


45
46
47
# File 'lib/rubino/tools/base.rb', line 45

def cancellation_requested?
  @cancel_token&.cancelled?
end

#config_keyObject

The ‘tools.<key>` config gate that enables/disables this tool. Single source of truth shared with Registry#tool_enabled_in_config? and the `tools` CLI command, so the displayed state always matches the state the registry actually enforces. Defaults to the tool’s own name; tools whose config key differs (webfetch/websearch both gate on ‘tools.web`) override this. Returning a key absent from config means the tool is enabled (opt-out model).



61
62
63
# File 'lib/rubino/tools/base.rb', line 61

def config_key
  name
end

#descriptionObject

Returns a description for the LLM

Raises:

  • (NotImplementedError)


66
67
68
# File 'lib/rubino/tools/base.rb', line 66

def description
  raise NotImplementedError, "#{self.class}#description not implemented"
end

#emit_chunk(text) ⇒ Object

Convenience guard so tools don’t sprinkle nil-checks at every emit.



36
37
38
39
40
# File 'lib/rubino/tools/base.rb', line 36

def emit_chunk(text)
  return if text.nil? || text.to_s.empty?

  @stream_chunk&.call(text.to_s)
end

#input_schemaObject

Returns the JSON schema for input parameters

Raises:

  • (NotImplementedError)


71
72
73
# File 'lib/rubino/tools/base.rb', line 71

def input_schema
  raise NotImplementedError, "#{self.class}#input_schema not implemented"
end

#nameObject

Returns the tool name (used in LLM tool definitions)

Raises:

  • (NotImplementedError)


50
51
52
# File 'lib/rubino/tools/base.rb', line 50

def name
  raise NotImplementedError, "#{self.class}#name not implemented"
end

#risk_levelObject

Returns the risk level: :low, :medium, :high



76
77
78
# File 'lib/rubino/tools/base.rb', line 76

def risk_level
  :low
end

#risky?Boolean

Returns true if this tool requires user confirmation

Returns:

  • (Boolean)


86
87
88
# File 'lib/rubino/tools/base.rb', line 86

def risky?
  %i[medium high].include?(risk_level)
end

#to_tool_definitionObject

Returns the tool definition for LLM registration



91
92
93
94
95
96
97
# File 'lib/rubino/tools/base.rb', line 91

def to_tool_definition
  {
    name: name,
    description: description,
    parameters: input_schema
  }
end