Class: Kward::Workspace
- Inherits:
-
Object
- Object
- Kward::Workspace
- Defined in:
- lib/kward/workspace.rb
Constant Summary collapse
- MAX_FILE_BYTES =
256 * 1024
- MAX_READ_OUTPUT_BYTES =
50 * 1024
- MAX_READ_OUTPUT_LINES =
2_000- MAX_COMMAND_OUTPUT_BYTES =
20 * 1024
- MAX_EDIT_DIFF_BYTES =
8 * 1024
- DEFAULT_COMMAND_TIMEOUT_SECONDS =
30
Instance Attribute Summary collapse
-
#root ⇒ Object
readonly
Returns the value of attribute root.
Instance Method Summary collapse
- #edit_file(path, edits, read_paths:) ⇒ Object
-
#initialize(root: Dir.pwd, max_file_bytes: MAX_FILE_BYTES, max_read_output_bytes: MAX_READ_OUTPUT_BYTES, max_read_output_lines: MAX_READ_OUTPUT_LINES, max_command_output_bytes: MAX_COMMAND_OUTPUT_BYTES) ⇒ Workspace
constructor
A new instance of Workspace.
- #list_directory(path) ⇒ Object
- #read_file(path, offset: nil, limit: nil) ⇒ Object
- #resolved_path(path) ⇒ Object
- #run_shell_command(command, timeout_seconds: DEFAULT_COMMAND_TIMEOUT_SECONDS, cancellation: nil) ⇒ Object
- #write_file(path, content, read_paths:) ⇒ Object
Constructor Details
#initialize(root: Dir.pwd, max_file_bytes: MAX_FILE_BYTES, max_read_output_bytes: MAX_READ_OUTPUT_BYTES, max_read_output_lines: MAX_READ_OUTPUT_LINES, max_command_output_bytes: MAX_COMMAND_OUTPUT_BYTES) ⇒ Workspace
Returns a new instance of Workspace.
15 16 17 18 19 20 21 |
# File 'lib/kward/workspace.rb', line 15 def initialize(root: Dir.pwd, max_file_bytes: MAX_FILE_BYTES, max_read_output_bytes: MAX_READ_OUTPUT_BYTES, max_read_output_lines: MAX_READ_OUTPUT_LINES, max_command_output_bytes: MAX_COMMAND_OUTPUT_BYTES) @root = Pathname.new(root).realpath @max_file_bytes = max_file_bytes @max_read_output_bytes = max_read_output_bytes @max_read_output_lines = max_read_output_lines @max_command_output_bytes = max_command_output_bytes end |
Instance Attribute Details
#root ⇒ Object (readonly)
Returns the value of attribute root.
23 24 25 |
# File 'lib/kward/workspace.rb', line 23 def root @root end |
Instance Method Details
#edit_file(path, edits, read_paths:) ⇒ Object
68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 |
# File 'lib/kward/workspace.rb', line 68 def edit_file(path, edits, read_paths:) resolved = workspace_path(path) return "Error: not a file: #{path}" unless File.file?(resolved) return "Error: existing file must be read before editing: #{path}" unless read_paths.include?(resolved) size = File.size(resolved) return "Error: file too large: #{path} is #{size} bytes; limit is #{@max_file_bytes} bytes" if size > @max_file_bytes content = File.read(resolved) result = apply_edits(path, content, edits) return result[:error] if result[:error] File.write(resolved, result[:content]) "Edited #{path}: replaced #{result[:count]} block(s)\n#{truncated_diff(path, content, result[:content])}" rescue SecurityError, Errno::ENOENT => e "Error: #{e.}" end |
#list_directory(path) ⇒ Object
25 26 27 28 29 30 31 32 33 34 |
# File 'lib/kward/workspace.rb', line 25 def list_directory(path) resolved = workspace_path(path) return "Error: not a directory: #{path}" unless File.directory?(resolved) Dir.children(resolved).sort.map do |entry| File.directory?(File.join(resolved, entry)) ? "#{entry}/" : entry end.join("\n") rescue SecurityError, Errno::ENOENT => e "Error: #{e.}" end |
#read_file(path, offset: nil, limit: nil) ⇒ Object
36 37 38 39 40 41 42 43 44 45 46 |
# File 'lib/kward/workspace.rb', line 36 def read_file(path, offset: nil, limit: nil) resolved = workspace_path(path) return "Error: not a file: #{path}" unless File.file?(resolved) size = File.size(resolved) return "Error: file too large: #{path} is #{size} bytes; limit is #{@max_file_bytes} bytes" if size > @max_file_bytes read_file_slice(File.read(resolved), offset: offset, limit: limit) rescue SecurityError, Errno::ENOENT => e "Error: #{e.}" end |
#resolved_path(path) ⇒ Object
116 117 118 |
# File 'lib/kward/workspace.rb', line 116 def resolved_path(path) workspace_path(path) end |
#run_shell_command(command, timeout_seconds: DEFAULT_COMMAND_TIMEOUT_SECONDS, cancellation: nil) ⇒ Object
86 87 88 89 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 |
# File 'lib/kward/workspace.rb', line 86 def run_shell_command(command, timeout_seconds: DEFAULT_COMMAND_TIMEOUT_SECONDS, cancellation: nil) command = command.to_s.strip return "Error: command is required" if command.empty? timeout_seconds = timeout_seconds.to_i timeout_seconds = DEFAULT_COMMAND_TIMEOUT_SECONDS if timeout_seconds <= 0 cancellation&.raise_if_cancelled! Open3.popen3(command, chdir: @root.to_s) do |stdin, stdout, stderr, wait_thread| stdin.close stdout_reader = Thread.new { stdout.read } stderr_reader = Thread.new { stderr.read } cancellation&.on_cancel { terminate_process(wait_thread.pid) } status = wait_for_process(wait_thread, timeout_seconds, cancellation) output = +"Exit status: #{status.exitstatus}\n" output << "\nSTDOUT:\n#{stdout_reader.value}" unless stdout_reader.value.empty? output << "\nSTDERR:\n#{stderr_reader.value}" unless stderr_reader.value.empty? truncate_output(output) rescue Timeout::Error terminate_process(wait_thread.pid) "Error: command timed out after #{timeout_seconds} seconds" ensure stdout_reader&.kill if stdout_reader&.alive? stderr_reader&.kill if stderr_reader&.alive? end rescue Errno::ENOENT, ArgumentError => e "Error: #{e.}" end |
#write_file(path, content, read_paths:) ⇒ Object
48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 |
# File 'lib/kward/workspace.rb', line 48 def write_file(path, content, read_paths:) resolved = workspace_write_path(path) if File.exist?(resolved) && !read_paths.include?(resolved) return "Error: existing file must be read before writing: #{path}" end if block_given? && !yield(relative_path(resolved), content.bytesize) return "Declined: write_file was not approved for #{path}" end old_content = File.exist?(resolved) ? File.read(resolved) : nil File.write(resolved, content) output = "Wrote #{content.bytesize} bytes to #{path}" output << "\n#{truncated_diff(path, old_content, content)}" if old_content && old_content != content output rescue SecurityError, Errno::ENOENT => e "Error: #{e.}" end |