Module: RobotLab::ScriptTool
- Defined in:
- lib/robot_lab/script_tool.rb
Overview
Factory module for wrapping AgentSkills scripts as RobotLab::Tool instances.
Given a path to an executable shell script, produces a Tool that shells out to the script and returns its combined stdout+stderr output. Non-executable scripts return nil with a logged warning.
Class Method Summary collapse
-
.derive_name(path) ⇒ String
Snake_case tool name derived from filename.
-
.execute(cmd, capabilities:, skill_dir:) ⇒ String
Run a command, optionally confined by the sandbox and a timeout.
-
.extract_description(path) ⇒ String
Extract tool description from the first non-shebang comment line.
- .format_result(output, status) ⇒ Object
-
.from_path(script_path, capabilities: nil, skill_dir: nil) ⇒ RobotLab::Tool?
Wrap a script file as a RobotLab::Tool.
-
.run_with_timeout(cmd, timeout) ⇒ Array(String, Process::Status|nil)
Output and status (nil = timed out).
- .terminate(pid) ⇒ Object
Class Method Details
.derive_name(path) ⇒ String
Returns snake_case tool name derived from filename.
109 110 111 112 113 114 |
# File 'lib/robot_lab/script_tool.rb', line 109 def self.derive_name(path) path.basename.to_s .sub(/\.[^.]+$/, '') .gsub(/[^a-zA-Z0-9]+/, '_') .gsub(/^_+|_+$/, '') end |
.execute(cmd, capabilities:, skill_dir:) ⇒ String
Run a command, optionally confined by the sandbox and a timeout.
When sandboxing is disabled (the default) this is the original, unconfined capture2e path with no timeout — behaviour is unchanged. When enabled, the command is wrapped by the sandbox strategy for the effective grant and bounded by the grant's timeout.
63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 |
# File 'lib/robot_lab/script_tool.rb', line 63 def self.execute(cmd, capabilities:, skill_dir:) unless Sandbox.enabled? output, status = Open3.capture2e(*cmd) return format_result(output, status) end grant = capabilities.intersect(Capabilities.ceiling) sandbox = Sandbox.for(grant, skill_dir: skill_dir) begin output, status = run_with_timeout(sandbox.wrap(cmd), grant.timeout) format_result(output, status) ensure sandbox.cleanup end end |
.extract_description(path) ⇒ String
Extract tool description from the first non-shebang comment line.
120 121 122 123 124 125 126 127 128 129 130 131 132 |
# File 'lib/robot_lab/script_tool.rb', line 120 def self.extract_description(path) File.foreach(path) do |line| stripped = line.strip next unless stripped.start_with?('#') next if stripped.start_with?('#!') # skip shebang desc = stripped.sub(/^#+\s*/, '').strip return desc unless desc.empty? end derive_name(path) rescue StandardError derive_name(path) end |
.format_result(output, status) ⇒ Object
101 102 103 104 105 |
# File 'lib/robot_lab/script_tool.rb', line 101 def self.format_result(output, status) return "Error (timed out):\n#{output}" if status.nil? status.success? ? output : "Error (exit #{status.exitstatus}):\n#{output}" end |
.from_path(script_path, capabilities: nil, skill_dir: nil) ⇒ RobotLab::Tool?
Wrap a script file as a RobotLab::Tool.
22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 |
# File 'lib/robot_lab/script_tool.rb', line 22 def self.from_path(script_path, capabilities: nil, skill_dir: nil) path = Pathname.new(script_path) unless path.executable? RobotLab.config.logger.warn( "ScriptTool: #{path.basename} is not executable, skipping" ) return nil end tool_name = derive_name(path) description = extract_description(path) script = path.to_s caps = capabilities || Capabilities.new dir = skill_dir || path.dirname.to_s Tool.create( name: tool_name, description: description, parameters: { type: 'object', properties: { args: { type: 'string', description: 'Optional command-line arguments' } }, required: [] } ) do |tool_args| cli_args = tool_args[:args].to_s.strip cmd = cli_args.empty? ? ['bash', script] : ['bash', script, *Shellwords.split(cli_args)] ScriptTool.execute(cmd, capabilities: caps, skill_dir: dir) end end |
.run_with_timeout(cmd, timeout) ⇒ Array(String, Process::Status|nil)
Returns output and status (nil = timed out).
80 81 82 83 84 85 86 87 88 89 90 91 92 |
# File 'lib/robot_lab/script_tool.rb', line 80 def self.run_with_timeout(cmd, timeout) Open3.popen2e(*cmd, pgroup: true) do |stdin, out, wait| stdin.close output = +'' begin Timeout.timeout(timeout) { output << out.read } rescue Timeout::Error terminate(wait.pid) return ["#{output}\n[killed: exceeded #{timeout}s]", nil] end [output, wait.value] end end |
.terminate(pid) ⇒ Object
94 95 96 97 98 |
# File 'lib/robot_lab/script_tool.rb', line 94 def self.terminate(pid) Process.kill('-TERM', Process.getpgid(pid)) rescue StandardError nil end |