Module: Legate::Generators::RuntimeToolLoader

Defined in:
lib/legate/generators/runtime_tool_loader.rb

Overview

Loads an AI-generated custom tool into the RUNNING process.

SECURITY: this executes LLM-generated Ruby in-process. Ruby has no true in-process sandbox, and CodeValidator is a denylist (blocks system/exec/eval/ popen/Open3) — not a jail. This path is therefore gated three ways:

1. Config: Legate.config.allow_runtime_tool_load (default ON outside prod).
2. The web UI requires an explicit per-tool "this runs code" confirmation.
3. The source is re-validated here, server-side, before loading.

All loads are serialized through LOAD_MUTEX and wrapped in a broad rescue so a bad generated tool can never crash the server. The tool is written to tools/ so it is auditable in source control and re-loaded on next boot.

Constant Summary collapse

LOAD_MUTEX =
Mutex.new

Class Method Summary collapse

Class Method Details

.enabled?Boolean

Returns whether runtime tool loading is permitted by config.

Returns:

  • (Boolean)

    whether runtime tool loading is permitted by config.



25
26
27
# File 'lib/legate/generators/runtime_tool_loader.rb', line 25

def enabled?
  Legate.config.allow_runtime_tool_load
end

.load_source!(source, suggested_name:) ⇒ Hash

Validate, persist to tools/<name>.rb, and load the tool into this process. Never raises — always returns a result hash.

Parameters:

  • source (String)

    the generated Ruby tool source.

  • suggested_name (String)

    basis for the file name.

Returns:

  • (Hash)

    { ok: true, tool_name:, path: } or { ok: false, error: }



34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
# File 'lib/legate/generators/runtime_tool_loader.rb', line 34

def load_source!(source, suggested_name:)
  return { ok: false, error: 'Runtime tool loading is disabled in this environment.' } unless enabled?

  CodeValidator.validate!(source)

  name = sanitize_name(suggested_name)
  return { ok: false, error: 'Could not derive a valid tool file name.' } if name.empty?

  path = File.join(tools_dir, "#{name}.rb")
  before = Legate::GlobalToolManager.registered_tool_names

  LOAD_MUTEX.synchronize do
    FileUtils.mkdir_p(tools_dir)
    File.write(path, source)
    # `load` (not `require`) so re-generating the same tool reloads it.
    load(path)
  end

  added = Legate::GlobalToolManager.registered_tool_names - before
  return { ok: false, error: 'The generated code did not register a tool (missing GlobalToolManager.register_tool call).' } if added.empty?

  { ok: true, tool_name: added.first.to_s, path: path }
rescue CodeValidator::UnsafeCodeError => e
  { ok: false, error: e.message }
rescue Exception => e # rubocop:disable Lint/RescueException
  # Broad on purpose: generated code can raise SyntaxError/NameError/LoadError
  # at file scope. A bad tool must never take down the server.
  Legate.logger.error("RuntimeToolLoader failed for '#{suggested_name}': #{e.class} - #{e.message}")
  { ok: false, error: "#{e.class}: #{e.message}" }
end

.sanitize_name(raw) ⇒ Object



71
72
73
# File 'lib/legate/generators/runtime_tool_loader.rb', line 71

def sanitize_name(raw)
  raw.to_s.strip.sub(/\A:/, '').downcase.gsub(/[^a-z0-9_]+/, '_').gsub(/\A_+|_+\z/, '')
end

.tools_dirObject

Where generated tools are written. Matches the boot loader’s ‘tools/` glob (TOOL_DIRECTORIES) so the tool is re-loaded on the next server start.



67
68
69
# File 'lib/legate/generators/runtime_tool_loader.rb', line 67

def tools_dir
  File.join(Dir.pwd, 'tools')
end