aigency 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +27 -0
- data/README.md +7 -15
- data/lib/aigency/aigent/current_time.rb +33 -0
- data/lib/aigency/aigent/shell.rb +108 -0
- data/lib/aigency/aigent.rb +52 -35
- data/lib/aigency/delegator.rb +11 -0
- data/lib/aigency/plugin/shell.rb +11 -0
- data/lib/aigency/plugin.rb +6 -2
- data/lib/aigency/rake.rb +13 -0
- data/lib/aigency/sandbox/exec.rb +2 -2
- data/lib/aigency/sandbox.rb +5 -4
- data/lib/aigency/shell.rb +4 -11
- data/lib/aigency/tempdir/helpers.rb +15 -0
- data/lib/aigency/tempdir/list.rb +17 -0
- data/lib/aigency/tempdir/read.rb +32 -0
- data/lib/aigency/tempdir/size.rb +27 -0
- data/lib/aigency/tempdir/write.rb +28 -0
- data/lib/aigency/tempdir.rb +19 -0
- data/lib/aigency/tool.rb +5 -0
- data/lib/aigency/usability.rb +1 -0
- data/lib/aigency/version.rb +1 -1
- data/lib/aigency/workspace/cat.rb +2 -2
- data/lib/aigency/workspace/{shell.rb → cmd.rb} +2 -2
- data/lib/aigency/workspace/copy_tempdir_file.rb +28 -0
- data/lib/aigency/workspace/cp.rb +2 -2
- data/lib/aigency/workspace/diff.rb +2 -2
- data/lib/aigency/workspace/grep.rb +2 -2
- data/lib/aigency/workspace/head.rb +2 -2
- data/lib/aigency/workspace/ls.rb +2 -2
- data/lib/aigency/workspace/mkdir.rb +2 -2
- data/lib/aigency/workspace/mv.rb +2 -2
- data/lib/aigency/workspace/rm.rb +2 -2
- data/lib/aigency/workspace/tail.rb +2 -2
- data/lib/aigency/workspace/wc.rb +2 -2
- data/lib/aigency/workspace/write.rb +2 -2
- data/lib/aigency/workspace.rb +3 -4
- data/lib/aigency.rb +16 -14
- data/scaffold/{Rakefile → aigent/aigent.rake} +1 -10
- data/scaffold/{.gitignore → enlist/.gitignore} +1 -0
- data/scaffold/{README.md → enlist/README.md} +7 -3
- data/scaffold/enlist/Rakefile +9 -0
- data/scaffold/enlist/bin/aigent +11 -0
- data/scaffold/{config → enlist/config}/schedule.rb +5 -0
- data/scaffold/sandbox/Dockerfile +37 -0
- data/scaffold/sandbox/docker-compose.yml +37 -0
- data/{lib/aigency/sandbox/squid.conf.tpl → scaffold/sandbox/squid.conf} +4 -2
- data/scaffold/sandbox/ssh_config +2 -0
- metadata +43 -15
- data/lib/aigency/sandbox/Dockerfile.tpl +0 -13
- data/lib/aigency/sandbox/docker-compose.yml.tpl +0 -26
- data/scaffold/config/aigent.yml +0 -9
- /data/scaffold/{.env.example → enlist/.env.example} +0 -0
- /data/scaffold/{Gemfile → enlist/Gemfile} +0 -0
- /data/scaffold/{IDENTITY.md → enlist/IDENTITY.md} +0 -0
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 4fce24d7da34c83fac5e0d3ce82d8bb79c31bede2b99ddfacec3b7efa4d157e3
|
|
4
|
+
data.tar.gz: 1a36424698a8a9b4bba0a0a0cf6a14e9b4cf8318a40a166893be3e093c909f9e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 9ce7989e062eb10afbb767d3e61f297ddf521f0b7c5fe0459c8021428843e6b05069db1ae88afbda7c29ab4ea0ca9f9f277b7472393f085f87dc5b36866cd786
|
|
7
|
+
data.tar.gz: 9a7a1efaad6467200964715b3db7920b52cc1cc39c7292fd8a52a2b4d4d023b16502ea511ee6824664970fecbdab19b2529dd95a2a5d0a8dfc49f9c53a5e062d
|
data/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,30 @@
|
|
|
1
|
+
## 0.4.0 - October 21, 2026
|
|
2
|
+
- Config file directory
|
|
3
|
+
- Add openssh to sandbox
|
|
4
|
+
- Sandbox configuration
|
|
5
|
+
- Tempdir tools
|
|
6
|
+
- Aigent plugin
|
|
7
|
+
- Proxy environment variables
|
|
8
|
+
- apk update && apk upgrade
|
|
9
|
+
- Plugins add rake files
|
|
10
|
+
- Aigent setup in README
|
|
11
|
+
- Rearrange sandbox scaffold
|
|
12
|
+
- Add bin/aigent to aigent directory
|
|
13
|
+
- Fix grep spec for BusyBox
|
|
14
|
+
- CurrentTime tool
|
|
15
|
+
- Dockerfile packages and mise setup
|
|
16
|
+
- Instructions subcommand
|
|
17
|
+
- Aigent#stream errors write to error.log
|
|
18
|
+
- Aigent stream command
|
|
19
|
+
- Remove adduser recommendation
|
|
20
|
+
- Clarify Docker requirement
|
|
21
|
+
- Plugin shells
|
|
22
|
+
- Tempdir file size management
|
|
23
|
+
- CopyTempdirFile tool
|
|
24
|
+
- Aigent plugin setup
|
|
25
|
+
- Update chat calls for RubyLLM 2.0
|
|
26
|
+
- Use the chat completions protocol
|
|
27
|
+
|
|
1
28
|
## 0.3.0 - October 9, 2026
|
|
2
29
|
- New plugin architecture
|
|
3
30
|
- Workspace and Sandbox plugins
|
data/README.md
CHANGED
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
A framework for creating AI agents with RubyLLM.
|
|
4
4
|
|
|
5
|
-
##
|
|
5
|
+
## Requirements
|
|
6
6
|
|
|
7
7
|
- Ruby 3.2+
|
|
8
|
-
- Docker 29+
|
|
8
|
+
- Docker 29+ (required for sandbox)
|
|
9
9
|
|
|
10
10
|
(Note: Aigency is not tested on Windows. Some features are known to be Windows-incompatible.)
|
|
11
11
|
|
|
@@ -27,28 +27,20 @@ gem 'aigency'
|
|
|
27
27
|
|
|
28
28
|
### Creating a New Aigent
|
|
29
29
|
|
|
30
|
-
The recommended way to create an aigent is to give it a dedicated system user.
|
|
31
|
-
|
|
32
|
-
```bash
|
|
33
|
-
useradd aigent_user
|
|
34
|
-
sudo su - aigent_user
|
|
35
|
-
```
|
|
36
|
-
|
|
37
|
-
Once you're logged in as the aigent user, create the aigent directory:
|
|
38
|
-
|
|
39
30
|
```bash
|
|
40
31
|
aigency enlist
|
|
41
32
|
cd aigent
|
|
42
33
|
bundle install
|
|
43
34
|
rake setup
|
|
35
|
+
cp .env.example .env
|
|
44
36
|
```
|
|
45
37
|
|
|
46
|
-
The bare minimum configuration you need to do is setting `
|
|
38
|
+
The bare minimum configuration you need to do is setting `OPENAI_API_BASE` and `OPENAI_API_KEY` in `.env`.
|
|
47
39
|
|
|
48
|
-
Test the aigent with the `
|
|
40
|
+
Test the aigent with the `say` command.
|
|
49
41
|
|
|
50
42
|
```bash
|
|
51
|
-
|
|
43
|
+
bin/aigent say "How many fibers are intertwined in a shredded wheat biscuit?"
|
|
52
44
|
```
|
|
53
45
|
|
|
54
|
-
See the aigent's README (scaffold/README.md in this codebase)
|
|
46
|
+
See the aigent's README for more information (`README.md` in your aigent directory or `scaffold/enlist/README.md` in this codebase).
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'date'
|
|
4
|
+
|
|
5
|
+
module Aigency
|
|
6
|
+
class Aigent
|
|
7
|
+
class CurrentTime < Aigency::Tool
|
|
8
|
+
description 'Get the current time'
|
|
9
|
+
|
|
10
|
+
parameters do
|
|
11
|
+
any_of :format, description: "Date/time format" do
|
|
12
|
+
string enum: %w[iso8601 unix]
|
|
13
|
+
'iso8601'
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def execute(format: 'iso8601')
|
|
18
|
+
aigent.log.puts "[Aigent::CurrentTime] #{format}"
|
|
19
|
+
now = DateTime.now
|
|
20
|
+
case format
|
|
21
|
+
when 'iso8601'
|
|
22
|
+
{ success: true, current_time: now.iso8601 }
|
|
23
|
+
when 'unix'
|
|
24
|
+
{ success: true, current_time: now.to_time.to_i }
|
|
25
|
+
else
|
|
26
|
+
{ success: false, error: "Invalid format: #{format}" }
|
|
27
|
+
end
|
|
28
|
+
rescue StandardError => e
|
|
29
|
+
{ success: false, error: "[#{e.class}] #{e.message}"}
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'thor'
|
|
4
|
+
|
|
5
|
+
module Aigency
|
|
6
|
+
class Aigent
|
|
7
|
+
# Commands that can be executed in aigent directories.
|
|
8
|
+
#
|
|
9
|
+
class Shell < Thor
|
|
10
|
+
include Delegator
|
|
11
|
+
|
|
12
|
+
desc "say PROMPT", "Send a prompt to the aigent"
|
|
13
|
+
def say(prompt)
|
|
14
|
+
aigent.stream prompt
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
desc "load FILE", "Load a prompt from a file"
|
|
18
|
+
def load(file)
|
|
19
|
+
aigent.load filename
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
desc "setup", "Initialize aigent plugins"
|
|
23
|
+
def setup
|
|
24
|
+
aigent.setup
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
desc "tools", "Get a list of available tools"
|
|
28
|
+
def tools
|
|
29
|
+
puts aigent.tools.map(&:to_s).sort
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
desc "status", "Get the aigent's current status"
|
|
33
|
+
def status
|
|
34
|
+
puts "Name: #{aigent.config('aigent')['name']}"
|
|
35
|
+
puts "Busy: #{aigent.busy?}"
|
|
36
|
+
if aigent.busy?
|
|
37
|
+
puts "Process: #{File.read(aigent.data('lock'))}"
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
desc "instructions", "List base instructions"
|
|
42
|
+
long_desc <<~DESC
|
|
43
|
+
Show all instructions that will be given to the aigent before the user
|
|
44
|
+
prompt. They typically include the IDENTITY.md file and any prompts injected
|
|
45
|
+
by installed plugins.
|
|
46
|
+
DESC
|
|
47
|
+
def instructions
|
|
48
|
+
puts aigent.instructions
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
desc "stream", "Tail the currently active chat stream"
|
|
52
|
+
option :follow, type: :boolean, aliases: :f, desc: "Follow the stream (tail -f)"
|
|
53
|
+
option :lines, type: :numeric, aliases: :n, desc: "Number of lines (tail -n)", default: 10
|
|
54
|
+
long_desc <<~DESC
|
|
55
|
+
This is useful for monitoring non-interactive chat streams, e.g., chat sessions
|
|
56
|
+
that were started by cron jobs or rake tasks. It runs `tail` on the current chat
|
|
57
|
+
file in the aigent's data/chats folder.
|
|
58
|
+
DESC
|
|
59
|
+
def stream
|
|
60
|
+
if aigent.busy?
|
|
61
|
+
file = Dir.glob(aigent.data('chats', '*.txt')).max { |file| File.mtime(file).to_i }
|
|
62
|
+
cmd = ['tail', file, '-n', options[:lines].to_s]
|
|
63
|
+
cmd.push '-f' if options[:follow]
|
|
64
|
+
puts "(#{file})"
|
|
65
|
+
exec *cmd
|
|
66
|
+
else
|
|
67
|
+
puts "No active chat stream."
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
desc "kill", "Kill a blocking process"
|
|
72
|
+
long_desc <<~DESC
|
|
73
|
+
Kill the process that owns an exclusive lock on the aigent's data/lock file.
|
|
74
|
+
DESC
|
|
75
|
+
def kill
|
|
76
|
+
if aigent.busy?
|
|
77
|
+
pid = File.read(aigent.data('lock')).to_i
|
|
78
|
+
puts "Killing process #{pid}"
|
|
79
|
+
Process.kill 'TERM', pid
|
|
80
|
+
else
|
|
81
|
+
puts "No blocking process exists."
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
class << self
|
|
86
|
+
def mounted? = false
|
|
87
|
+
|
|
88
|
+
def start(args = ARGV, config = {})
|
|
89
|
+
return super if mounted?
|
|
90
|
+
build('.').start(args, config)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def build(directory)
|
|
94
|
+
Class.new(self) do
|
|
95
|
+
def self.mounted? = true
|
|
96
|
+
Aigent.new(directory).plugins.each do |plugin|
|
|
97
|
+
next unless plugin.shell
|
|
98
|
+
if all_commands.key?(plugin.name)
|
|
99
|
+
raise "Plugin '#{plugin.name}' cannot register CLI command '#{plugin.name}': already taken"
|
|
100
|
+
end
|
|
101
|
+
register(plugin.shell, plugin.name, "#{plugin.name} [SUBCOMMAND ...ARGS]", "#{plugin.name} plugin commands")
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
data/lib/aigency/aigent.rb
CHANGED
|
@@ -6,13 +6,15 @@ require 'set'
|
|
|
6
6
|
|
|
7
7
|
module Aigency
|
|
8
8
|
class Aigent
|
|
9
|
+
require 'aigency/aigent/current_time'
|
|
10
|
+
require 'aigency/aigent/shell'
|
|
11
|
+
|
|
9
12
|
# @return [IO]
|
|
10
13
|
attr_reader :log
|
|
11
14
|
|
|
12
15
|
def initialize(directory)
|
|
13
16
|
@base_directory = File.absolute_path(directory)
|
|
14
17
|
@log = $stdout
|
|
15
|
-
load_requires
|
|
16
18
|
end
|
|
17
19
|
|
|
18
20
|
# @return [String]
|
|
@@ -42,10 +44,10 @@ module Aigency
|
|
|
42
44
|
# @param except [Array<Class<Tool>>] Tools to exclude from this chat
|
|
43
45
|
# @return [RubyLLM::Chat]
|
|
44
46
|
def brief(*extra, except: [])
|
|
45
|
-
chat = rubyllm_context.chat(provider: :openai)
|
|
46
|
-
(tools - except).each { |klass| chat.
|
|
47
|
-
chat.with_instructions(build_instructions(extra), append: true)
|
|
48
|
-
chat.
|
|
47
|
+
chat = rubyllm_context.chat(provider: :openai, protocol: :chat_completions)
|
|
48
|
+
(tools - except).each { |klass| chat.with_tools(klass.new(self)) }
|
|
49
|
+
chat.with_instructions(build_instructions(extra).join, append: true)
|
|
50
|
+
chat.with_provider_options(max_tokens: max_tokens)
|
|
49
51
|
end
|
|
50
52
|
|
|
51
53
|
# Prompt a chat with a file.
|
|
@@ -69,16 +71,17 @@ module Aigency
|
|
|
69
71
|
def stream text, *extra, attach: [], except: [], out: $stdout
|
|
70
72
|
original_log = @log
|
|
71
73
|
@log = out
|
|
72
|
-
response = brief(*extra, except: except).ask text, with: attach do |chunk|
|
|
73
|
-
@log.print chunk.thinking&.text
|
|
74
|
-
|
|
75
|
-
|
|
74
|
+
response = brief(*extra, except: except).ask text, with: attach #do |chunk|
|
|
75
|
+
# @log.print chunk.thinking&.text
|
|
76
|
+
#end
|
|
77
|
+
@log.puts response.content
|
|
76
78
|
@log.puts "Input tokens: #{response.tokens.input}"
|
|
77
79
|
@log.puts "Output tokens: #{response.tokens.output}"
|
|
78
80
|
response
|
|
79
81
|
rescue StandardError => e
|
|
80
|
-
|
|
81
|
-
@log.puts
|
|
82
|
+
message = "[#{e.class}] #{e.message}\n" + e.backtrace.map { |line| " #{line}\n" }.join
|
|
83
|
+
@log.puts message
|
|
84
|
+
File.write data('log', 'error.log'), "#{DateTime.now.iso8601} #{message}", mode: 'a'
|
|
82
85
|
ensure
|
|
83
86
|
@log = original_log
|
|
84
87
|
end
|
|
@@ -98,7 +101,7 @@ module Aigency
|
|
|
98
101
|
|
|
99
102
|
# Run code in an exclusive lock
|
|
100
103
|
#
|
|
101
|
-
def
|
|
104
|
+
def block(&block)
|
|
102
105
|
File.open(data('lock'), 'w') do |file|
|
|
103
106
|
file.flock(File::LOCK_EX)
|
|
104
107
|
file.write(Process.pid)
|
|
@@ -106,6 +109,8 @@ module Aigency
|
|
|
106
109
|
block&.call
|
|
107
110
|
end
|
|
108
111
|
end
|
|
112
|
+
alias lock block
|
|
113
|
+
alias flock block
|
|
109
114
|
|
|
110
115
|
# Run code if an exclusive lock is available or skip if a lock exists
|
|
111
116
|
#
|
|
@@ -130,33 +135,27 @@ module Aigency
|
|
|
130
135
|
end
|
|
131
136
|
alias busy? locked?
|
|
132
137
|
|
|
138
|
+
# @return [String]
|
|
133
139
|
def instructions
|
|
134
|
-
@instructions ||= build_instructions([])
|
|
140
|
+
@instructions ||= build_instructions([]).join("\n\n")
|
|
135
141
|
end
|
|
136
142
|
|
|
137
143
|
# @return [Array<Plugin>]
|
|
138
144
|
def plugins
|
|
139
145
|
config('aigent').fetch('plugins', [])
|
|
146
|
+
.unshift('aigent')
|
|
147
|
+
.uniq
|
|
140
148
|
.map { |name| Plugin.registry[name] }
|
|
141
149
|
end
|
|
142
150
|
|
|
143
151
|
def tools
|
|
144
|
-
plugins.flat_map(&:tools)
|
|
152
|
+
plugins.flat_map(&:tools).uniq
|
|
145
153
|
end
|
|
146
154
|
|
|
147
155
|
private
|
|
148
156
|
|
|
149
157
|
# @param plugin [Plugin]
|
|
150
158
|
def boot(plugin)
|
|
151
|
-
unless plugin.config.empty?
|
|
152
|
-
config_file = File.absolute_path("#{plugin.name}.yml", directory)
|
|
153
|
-
if File.exist?(config_file)
|
|
154
|
-
log.puts "File #{plugin.name}.yml exists, skipping"
|
|
155
|
-
else
|
|
156
|
-
log.puts "Creating #{plugin.name}.yml"
|
|
157
|
-
File.write config_file, plugin.config
|
|
158
|
-
end
|
|
159
|
-
end
|
|
160
159
|
plugin.directories.each do |dir|
|
|
161
160
|
absolute = File.absolute_path(dir, directory)
|
|
162
161
|
if File.directory?(absolute)
|
|
@@ -166,6 +165,15 @@ module Aigency
|
|
|
166
165
|
FileUtils.mkdir_p absolute
|
|
167
166
|
end
|
|
168
167
|
end
|
|
168
|
+
unless plugin.config.empty?
|
|
169
|
+
config_file = File.absolute_path(File.join('config', "#{plugin.name}.yml"), directory)
|
|
170
|
+
if File.exist?(config_file)
|
|
171
|
+
log.puts "File config/#{plugin.name}.yml exists, skipping"
|
|
172
|
+
else
|
|
173
|
+
log.puts "Creating config/#{plugin.name}.yml"
|
|
174
|
+
File.write config_file, plugin.config
|
|
175
|
+
end
|
|
176
|
+
end
|
|
169
177
|
plugin.files.each do |file, content|
|
|
170
178
|
absolute = File.absolute_path(file, directory)
|
|
171
179
|
if File.exist?(absolute)
|
|
@@ -211,11 +219,10 @@ module Aigency
|
|
|
211
219
|
@base_instructions ||= identity_file? ? File.read(identity_file) : "You are a helpful AI assistant."
|
|
212
220
|
end
|
|
213
221
|
|
|
222
|
+
# @return [Array<String>]
|
|
214
223
|
def build_instructions(extra)
|
|
215
|
-
[base_instructions].concat
|
|
216
|
-
.
|
|
217
|
-
.concat extra
|
|
218
|
-
.join("\n\n")
|
|
224
|
+
[base_instructions].concat(tool_prompts)
|
|
225
|
+
.concat(extra)
|
|
219
226
|
end
|
|
220
227
|
|
|
221
228
|
def tool_prompts
|
|
@@ -225,14 +232,6 @@ module Aigency
|
|
|
225
232
|
.compact
|
|
226
233
|
end
|
|
227
234
|
|
|
228
|
-
def load_requires
|
|
229
|
-
read_requires.each { |name| require name }
|
|
230
|
-
end
|
|
231
|
-
|
|
232
|
-
def read_requires
|
|
233
|
-
config('aigent')['require'] || []
|
|
234
|
-
end
|
|
235
|
-
|
|
236
235
|
def max_tokens
|
|
237
236
|
@max_tokens ||= (config('aigent')['max_tokens'] || -1).to_i
|
|
238
237
|
end
|
|
@@ -243,5 +242,23 @@ module Aigency
|
|
|
243
242
|
config.openai_api_key = config('aigent')['openai_api_key']
|
|
244
243
|
end
|
|
245
244
|
end
|
|
245
|
+
|
|
246
|
+
Plugin.register Plugin.new(
|
|
247
|
+
'aigent',
|
|
248
|
+
tools: [CurrentTime],
|
|
249
|
+
directories: ['config', 'data/chats', 'data/log'],
|
|
250
|
+
rakefile: File.join(GEM_PATH, 'scaffold', 'aigent', 'aigent.rake'),
|
|
251
|
+
config: <<~FILE
|
|
252
|
+
name: Aigent
|
|
253
|
+
openai_api_base: ${OPENAI_API_BASE}
|
|
254
|
+
openai_api_key: ${OPENAI_API_KEY}
|
|
255
|
+
# max_tokens: 65536
|
|
256
|
+
plugins:
|
|
257
|
+
# You should only need either a workspace or a sandbox, not both. If you
|
|
258
|
+
# enable one, it's recommended that you disable the other.
|
|
259
|
+
# - sandbox
|
|
260
|
+
- workspace
|
|
261
|
+
FILE
|
|
262
|
+
)
|
|
246
263
|
end
|
|
247
264
|
end
|
data/lib/aigency/plugin.rb
CHANGED
|
@@ -2,20 +2,24 @@
|
|
|
2
2
|
|
|
3
3
|
module Aigency
|
|
4
4
|
class Plugin
|
|
5
|
-
|
|
5
|
+
require 'aigency/plugin/shell'
|
|
6
|
+
|
|
7
|
+
attr_reader :name, :config, :tools, :directories, :files, :rakefile, :shell
|
|
6
8
|
|
|
7
9
|
# @param name [String]
|
|
8
10
|
# @param tools [Array<Class<Tool>>]
|
|
9
11
|
# @param config [String]
|
|
10
12
|
# @param directories [Array<String>]
|
|
11
13
|
# @param files [Hash{String => String}]
|
|
12
|
-
def initialize name, tools: [], config: '', directories: [], files: {}
|
|
14
|
+
def initialize name, tools: [], config: '', directories: [], files: {}, rakefile: nil, shell: nil
|
|
13
15
|
raise "A plugin named '#{name}' already exists" if self.class.registry.key?(name)
|
|
14
16
|
@name = name
|
|
15
17
|
@config = config
|
|
16
18
|
@tools = tools
|
|
17
19
|
@directories = directories
|
|
18
20
|
@files = files
|
|
21
|
+
@rakefile = rakefile
|
|
22
|
+
@shell = shell
|
|
19
23
|
self.class.registry[name] = self
|
|
20
24
|
end
|
|
21
25
|
|
data/lib/aigency/rake.rb
ADDED
data/lib/aigency/sandbox/exec.rb
CHANGED
|
@@ -5,9 +5,9 @@ require 'open3'
|
|
|
5
5
|
module Aigency
|
|
6
6
|
module Sandbox
|
|
7
7
|
class Exec < Aigency::Tool
|
|
8
|
-
|
|
8
|
+
description 'Run a shell command in your sandbox'
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
parameters do
|
|
11
11
|
string :command, description: "The complete command to execute", required: true
|
|
12
12
|
end
|
|
13
13
|
|
data/lib/aigency/sandbox.rb
CHANGED
|
@@ -7,11 +7,12 @@ module Aigency
|
|
|
7
7
|
Plugin.register Plugin.new(
|
|
8
8
|
'sandbox',
|
|
9
9
|
tools: [Sandbox::Exec],
|
|
10
|
-
directories: ['data/sandbox', 'data/sandbox_ssh'],
|
|
10
|
+
directories: ['sandbox', 'data/sandbox', 'data/sandbox_ssh'],
|
|
11
11
|
files: {
|
|
12
|
-
'Dockerfile' => File.read(File.join(
|
|
13
|
-
'
|
|
14
|
-
'
|
|
12
|
+
'sandbox/Dockerfile' => File.read(File.join(GEM_PATH, 'scaffold', 'sandbox', 'Dockerfile')),
|
|
13
|
+
'sandbox/squid.conf' => File.read(File.join(GEM_PATH, 'scaffold', 'sandbox', 'squid.conf')),
|
|
14
|
+
'docker-compose.yml' => File.read(File.join(GEM_PATH, 'scaffold', 'sandbox', 'docker-compose.yml')),
|
|
15
|
+
'data/sandbox_ssh/config' => File.read(File.join(GEM_PATH, 'scaffold', 'sandbox', 'ssh_config'))
|
|
15
16
|
},
|
|
16
17
|
config: <<~FILE
|
|
17
18
|
prompt: "The sandbox tools provide access to a Docker container."
|
data/lib/aigency/shell.rb
CHANGED
|
@@ -4,9 +4,11 @@ require 'fileutils'
|
|
|
4
4
|
require 'thor'
|
|
5
5
|
|
|
6
6
|
module Aigency
|
|
7
|
+
# Top-level aigency commands for creating aigents.
|
|
8
|
+
#
|
|
7
9
|
class Shell < Thor
|
|
8
10
|
map %i[new] => :enlist
|
|
9
|
-
desc "enlist [DIRECTORY]", "
|
|
11
|
+
desc "enlist [DIRECTORY]", "Create an aigent directory"
|
|
10
12
|
def enlist directory = 'aigent'
|
|
11
13
|
puts "Installing aigent at #{directory}"
|
|
12
14
|
if File.exist?(directory)
|
|
@@ -18,16 +20,7 @@ module Aigency
|
|
|
18
20
|
end
|
|
19
21
|
end
|
|
20
22
|
FileUtils.mkdir(directory) unless File.directory?(directory)
|
|
21
|
-
FileUtils.copy_entry(File.join(GEM_PATH, 'scaffold'), directory)
|
|
23
|
+
FileUtils.copy_entry(File.join(GEM_PATH, 'scaffold', 'enlist'), directory)
|
|
22
24
|
end
|
|
23
|
-
|
|
24
|
-
# desc "setup PLUGIN [...PLUGIN]", "Initialize plugin files"
|
|
25
|
-
# option :all, type: :boolean, alias: :a, description: "Set up all plugins", default: false
|
|
26
|
-
# option :overwrite, type: :boolean, alias: :o, description: "Overwrite existing files", default: false
|
|
27
|
-
# def setup *plugins
|
|
28
|
-
# # @todo Assuming all for now
|
|
29
|
-
# aigent = Aigent.new(aigent)
|
|
30
|
-
# aigent.setup
|
|
31
|
-
# end
|
|
32
25
|
end
|
|
33
26
|
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Aigency
|
|
4
|
+
module Tempdir
|
|
5
|
+
module Helpers
|
|
6
|
+
module_function
|
|
7
|
+
|
|
8
|
+
def expand_in_tempdir path
|
|
9
|
+
absolute = File.absolute_path(path, PATH)
|
|
10
|
+
raise PathError, "#{path} is not relative to tempdir" unless absolute.start_with?(PATH)
|
|
11
|
+
absolute
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Aigency
|
|
4
|
+
module Tempdir
|
|
5
|
+
class List < Tool
|
|
6
|
+
description "List all files in the temp directory"
|
|
7
|
+
|
|
8
|
+
def execute
|
|
9
|
+
aigent.log.puts "[Tempdir::List]"
|
|
10
|
+
{
|
|
11
|
+
success: true,
|
|
12
|
+
absolute_paths: Dir.glob(File.join(PATH, '**', '*'))
|
|
13
|
+
}
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Aigency
|
|
4
|
+
module Tempdir
|
|
5
|
+
class Read < Tool
|
|
6
|
+
include Helpers
|
|
7
|
+
|
|
8
|
+
description "Read a file in the temp directory"
|
|
9
|
+
|
|
10
|
+
parameters do
|
|
11
|
+
string :filename, description: "The file to read"
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def execute(filename:)
|
|
15
|
+
aigent.log.puts "[Tempdir::Read] #{filename}"
|
|
16
|
+
absolute_path = expand_in_tempdir(filename)
|
|
17
|
+
# @todo Hardcoded max size
|
|
18
|
+
if File.size(absolute_path) > 500_000
|
|
19
|
+
{ success: false, error: "File is too large to read (max 500000 bytes)"}
|
|
20
|
+
else
|
|
21
|
+
{
|
|
22
|
+
success: true,
|
|
23
|
+
absolute_path: absolute_path,
|
|
24
|
+
content: File.read(File.join(PATH, filename))
|
|
25
|
+
}
|
|
26
|
+
end
|
|
27
|
+
rescue StandardError => e
|
|
28
|
+
{ success: false, error: "[#{e.class}] #{e.message}"}
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Aigency
|
|
4
|
+
module Tempdir
|
|
5
|
+
class Size < Tool
|
|
6
|
+
include Helpers
|
|
7
|
+
|
|
8
|
+
description "Get the size of a file in the temp directory"
|
|
9
|
+
|
|
10
|
+
parameters do
|
|
11
|
+
string :filename, description: "The file to measure"
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def execute(filename:)
|
|
15
|
+
aigent.log.puts "[Tempdir::Read] #{filename}"
|
|
16
|
+
absolute_path = expand_in_tempdir(filename)
|
|
17
|
+
{
|
|
18
|
+
success: true,
|
|
19
|
+
absolute_path: absolute_path,
|
|
20
|
+
size: File.size(absolute_path)
|
|
21
|
+
}
|
|
22
|
+
rescue StandardError => e
|
|
23
|
+
{ success: false, error: "[#{e.class}] #{e.message}"}
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Aigency
|
|
4
|
+
module Tempdir
|
|
5
|
+
class Write < Tool
|
|
6
|
+
include Helpers
|
|
7
|
+
|
|
8
|
+
description "Write a file in the temp directory"
|
|
9
|
+
|
|
10
|
+
parameters do
|
|
11
|
+
string :filename, description: "The filename to write"
|
|
12
|
+
string :content, description: "The file's content"
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def execute(filename:, content:)
|
|
16
|
+
aigent.log.puts "[Tempdir::Write] #{filename}"
|
|
17
|
+
absolute_path = expand_in_tempdir(filename)
|
|
18
|
+
File.write absolute_path, content
|
|
19
|
+
{
|
|
20
|
+
success: true,
|
|
21
|
+
absolute_path: absolute_path
|
|
22
|
+
}
|
|
23
|
+
rescue StandardError => e
|
|
24
|
+
{ success: false, error: "[#{e.class}] #{e.message}"}
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|