self_agency 0.0.1
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 +7 -0
- data/.envrc +1 -0
- data/.github/workflows/deploy-github-pages.yml +40 -0
- data/.irbrc +22 -0
- data/CHANGELOG.md +5 -0
- data/COMMITS.md +196 -0
- data/LICENSE.txt +21 -0
- data/README.md +177 -0
- data/Rakefile +8 -0
- data/docs/api/configuration.md +85 -0
- data/docs/api/errors.md +166 -0
- data/docs/api/index.md +37 -0
- data/docs/api/self-agency-module.md +198 -0
- data/docs/architecture/overview.md +181 -0
- data/docs/architecture/security.md +101 -0
- data/docs/assets/images/self_agency.gif +0 -0
- data/docs/assets/images/self_agency.mp4 +0 -0
- data/docs/development/contributing.md +45 -0
- data/docs/development/setup.md +81 -0
- data/docs/development/testing.md +70 -0
- data/docs/examples/autonomous-robots.md +109 -0
- data/docs/examples/basic-examples.md +237 -0
- data/docs/examples/collaborative-robots.md +98 -0
- data/docs/examples/full-workflow.md +100 -0
- data/docs/examples/index.md +36 -0
- data/docs/getting-started/installation.md +71 -0
- data/docs/getting-started/quick-start.md +94 -0
- data/docs/guide/configuration.md +113 -0
- data/docs/guide/generating-methods.md +146 -0
- data/docs/guide/how-to-use.md +144 -0
- data/docs/guide/lifecycle-hooks.md +86 -0
- data/docs/guide/prompt-templates.md +189 -0
- data/docs/guide/saving-methods.md +84 -0
- data/docs/guide/scopes.md +74 -0
- data/docs/guide/source-inspection.md +96 -0
- data/docs/index.md +77 -0
- data/examples/01_basic_usage.rb +27 -0
- data/examples/02_multiple_methods.rb +43 -0
- data/examples/03_scopes.rb +40 -0
- data/examples/04_source_inspection.rb +46 -0
- data/examples/05_lifecycle_hook.rb +55 -0
- data/examples/06_configuration.rb +97 -0
- data/examples/07_error_handling.rb +103 -0
- data/examples/08_class_context.rb +64 -0
- data/examples/09_method_override.rb +52 -0
- data/examples/10_full_workflow.rb +118 -0
- data/examples/11_collaborative_robots/atlas.rb +31 -0
- data/examples/11_collaborative_robots/echo.rb +30 -0
- data/examples/11_collaborative_robots/main.rb +190 -0
- data/examples/11_collaborative_robots/nova.rb +71 -0
- data/examples/11_collaborative_robots/robot.rb +119 -0
- data/examples/12_autonomous_robots/analyst.rb +193 -0
- data/examples/12_autonomous_robots/collector.rb +78 -0
- data/examples/12_autonomous_robots/main.rb +166 -0
- data/examples/12_autonomous_robots/planner.rb +125 -0
- data/examples/12_autonomous_robots/robot.rb +284 -0
- data/examples/generated/from_range_class.rb +3 -0
- data/examples/generated/mean_instance.rb +4 -0
- data/examples/generated/median_instance.rb +15 -0
- data/examples/generated/report_singleton.rb +3 -0
- data/examples/generated/standard_deviation_instance.rb +8 -0
- data/examples/lib/message_bus.rb +57 -0
- data/examples/lib/setup.rb +8 -0
- data/lib/self_agency/configuration.rb +76 -0
- data/lib/self_agency/errors.rb +35 -0
- data/lib/self_agency/generator.rb +47 -0
- data/lib/self_agency/prompts/generate/system.txt.erb +15 -0
- data/lib/self_agency/prompts/generate/user.txt.erb +13 -0
- data/lib/self_agency/prompts/shape/system.txt.erb +26 -0
- data/lib/self_agency/prompts/shape/user.txt.erb +10 -0
- data/lib/self_agency/sandbox.rb +17 -0
- data/lib/self_agency/saver.rb +62 -0
- data/lib/self_agency/validator.rb +64 -0
- data/lib/self_agency/version.rb +5 -0
- data/lib/self_agency.rb +315 -0
- data/mkdocs.yml +156 -0
- data/sig/self_agency.rbs +4 -0
- metadata +163 -0
data/docs/api/errors.md
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
# Errors
|
|
2
|
+
|
|
3
|
+
SelfAgency defines a hierarchy of errors under `SelfAgency::Error`.
|
|
4
|
+
|
|
5
|
+
## Hierarchy
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
StandardError
|
|
9
|
+
└── SelfAgency::Error
|
|
10
|
+
├── SelfAgency::GenerationError
|
|
11
|
+
├── SelfAgency::ValidationError
|
|
12
|
+
└── SelfAgency::SecurityError
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
All SelfAgency errors inherit from `SelfAgency::Error`, which inherits from `StandardError`. This means you can catch all SelfAgency errors with a single `rescue`:
|
|
16
|
+
|
|
17
|
+
```ruby
|
|
18
|
+
rescue SelfAgency::Error => e
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## `SelfAgency::Error`
|
|
24
|
+
|
|
25
|
+
Base error class. Also raised directly when configuration is missing.
|
|
26
|
+
|
|
27
|
+
**Raised when:**
|
|
28
|
+
|
|
29
|
+
- `_()` is called before `SelfAgency.configure`
|
|
30
|
+
- `_save!` is called with no generated methods
|
|
31
|
+
- `_save!` is called on an anonymous class
|
|
32
|
+
|
|
33
|
+
```ruby
|
|
34
|
+
SelfAgency.reset!
|
|
35
|
+
Widget.new._("a method")
|
|
36
|
+
#=> SelfAgency::Error: SelfAgency.configure has not been called
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
---
|
|
40
|
+
|
|
41
|
+
## `SelfAgency::GenerationError`
|
|
42
|
+
|
|
43
|
+
Raised when the LLM fails to produce output or when an LLM communication failure occurs.
|
|
44
|
+
|
|
45
|
+
**Attributes:**
|
|
46
|
+
|
|
47
|
+
| Attribute | Type | Description |
|
|
48
|
+
|-----------|------|-------------|
|
|
49
|
+
| `stage` | `Symbol` or `nil` | `:shape` or `:generate` -- which pipeline stage failed |
|
|
50
|
+
| `attempt` | `Integer` or `nil` | The attempt number (during retry loop) |
|
|
51
|
+
|
|
52
|
+
**Raised when:**
|
|
53
|
+
|
|
54
|
+
- The shape stage returns `nil` -- message: `"Prompt shaping failed (LLM returned nil)"`
|
|
55
|
+
- The generate stage returns `nil` -- message: `"Code generation failed (LLM returned nil)"`
|
|
56
|
+
- An LLM communication failure occurs -- message: `"LLM request failed (ExceptionClass: details)"`
|
|
57
|
+
|
|
58
|
+
!!! note
|
|
59
|
+
LLM communication failures (network errors, timeouts, provider API errors) are wrapped and re-raised as `GenerationError`. The original exception class and message are preserved in the error message. If generation consistently fails, verify your LLM provider is running and the configuration (provider, model, api_base) is correct.
|
|
60
|
+
|
|
61
|
+
```ruby
|
|
62
|
+
rescue SelfAgency::GenerationError => e
|
|
63
|
+
puts "LLM failed at #{e.stage} stage: #{e.message}"
|
|
64
|
+
end
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
---
|
|
68
|
+
|
|
69
|
+
## `SelfAgency::ValidationError`
|
|
70
|
+
|
|
71
|
+
Raised when the generated code fails structural or syntactic validation.
|
|
72
|
+
|
|
73
|
+
**Attributes:**
|
|
74
|
+
|
|
75
|
+
| Attribute | Type | Description |
|
|
76
|
+
|-----------|------|-------------|
|
|
77
|
+
| `generated_code` | `String` or `nil` | The code that failed validation |
|
|
78
|
+
| `attempt` | `Integer` or `nil` | The attempt number (during retry loop) |
|
|
79
|
+
|
|
80
|
+
**Raised when:**
|
|
81
|
+
|
|
82
|
+
- Generated code is empty after sanitization
|
|
83
|
+
- Generated code does not contain a `def...end` structure
|
|
84
|
+
- Generated code has a syntax error (`RubyVM::InstructionSequence.compile` fails)
|
|
85
|
+
|
|
86
|
+
!!! note
|
|
87
|
+
During automatic retries, `ValidationError` is only raised to the caller after all `generation_retries` attempts are exhausted. The `attempt` attribute indicates which attempt produced the final failure.
|
|
88
|
+
|
|
89
|
+
```ruby
|
|
90
|
+
# Empty code
|
|
91
|
+
widget.send(:self_agency_validate!, "")
|
|
92
|
+
#=> SelfAgency::ValidationError: code is empty
|
|
93
|
+
|
|
94
|
+
# Missing def...end
|
|
95
|
+
widget.send(:self_agency_validate!, "puts 'hello'")
|
|
96
|
+
#=> SelfAgency::ValidationError: missing def...end structure
|
|
97
|
+
|
|
98
|
+
# Syntax error
|
|
99
|
+
widget.send(:self_agency_validate!, "def broken\n if true\nend")
|
|
100
|
+
#=> SelfAgency::ValidationError: syntax error: ...
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
---
|
|
104
|
+
|
|
105
|
+
## `SelfAgency::SecurityError`
|
|
106
|
+
|
|
107
|
+
Raised when the generated code contains a dangerous pattern.
|
|
108
|
+
|
|
109
|
+
**Attributes:**
|
|
110
|
+
|
|
111
|
+
| Attribute | Type | Description |
|
|
112
|
+
|-----------|------|-------------|
|
|
113
|
+
| `matched_pattern` | `String` or `nil` | The specific pattern text that was matched |
|
|
114
|
+
| `generated_code` | `String` or `nil` | The code that triggered the error |
|
|
115
|
+
|
|
116
|
+
**Raised when:**
|
|
117
|
+
|
|
118
|
+
- The code matches `SelfAgency::DANGEROUS_PATTERNS` (static analysis)
|
|
119
|
+
|
|
120
|
+
The error message includes the specific matched pattern, e.g., `"dangerous pattern detected: system"`.
|
|
121
|
+
|
|
122
|
+
!!! note
|
|
123
|
+
This is `SelfAgency::SecurityError`, distinct from Ruby's built-in `::SecurityError`. The runtime sandbox raises `::SecurityError` (the Ruby built-in), while the static validator raises `SelfAgency::SecurityError`.
|
|
124
|
+
|
|
125
|
+
```ruby
|
|
126
|
+
# System call
|
|
127
|
+
widget.send(:self_agency_validate!, "def hack\n system('ls')\nend")
|
|
128
|
+
#=> SelfAgency::SecurityError: dangerous pattern detected: system
|
|
129
|
+
|
|
130
|
+
# File access
|
|
131
|
+
widget.send(:self_agency_validate!, "def hack\n File.read('/etc/passwd')\nend")
|
|
132
|
+
#=> SelfAgency::SecurityError: dangerous pattern detected: File.
|
|
133
|
+
|
|
134
|
+
# Eval
|
|
135
|
+
widget.send(:self_agency_validate!, "def hack\n eval('1+1')\nend")
|
|
136
|
+
#=> SelfAgency::SecurityError: dangerous pattern detected: eval
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
---
|
|
140
|
+
|
|
141
|
+
## Error Handling Patterns
|
|
142
|
+
|
|
143
|
+
### Catch All SelfAgency Errors
|
|
144
|
+
|
|
145
|
+
```ruby
|
|
146
|
+
begin
|
|
147
|
+
obj._("a method description")
|
|
148
|
+
rescue SelfAgency::Error => e
|
|
149
|
+
puts "#{e.class}: #{e.message}"
|
|
150
|
+
end
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
### Catch Specific Errors
|
|
154
|
+
|
|
155
|
+
```ruby
|
|
156
|
+
begin
|
|
157
|
+
obj._("a method description")
|
|
158
|
+
rescue SelfAgency::GenerationError => e
|
|
159
|
+
puts "LLM failed at #{e.stage} stage (attempt #{e.attempt}): #{e.message}"
|
|
160
|
+
rescue SelfAgency::ValidationError => e
|
|
161
|
+
puts "Validation failed on attempt #{e.attempt}: #{e.message}"
|
|
162
|
+
puts "Code was: #{e.generated_code}" if e.generated_code
|
|
163
|
+
rescue SelfAgency::SecurityError => e
|
|
164
|
+
puts "Security: matched '#{e.matched_pattern}' in generated code"
|
|
165
|
+
end
|
|
166
|
+
```
|
data/docs/api/index.md
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# API Reference
|
|
2
|
+
|
|
3
|
+
Complete reference for SelfAgency's public API.
|
|
4
|
+
|
|
5
|
+
## Modules and Classes
|
|
6
|
+
|
|
7
|
+
- [**SelfAgency Module**](self-agency-module.md) -- The main mixin module with `_()`, `_source_for`, `_save!`, and `on_method_generated`
|
|
8
|
+
- [**Configuration**](configuration.md) -- `SelfAgency::Configuration` class and singleton methods (`configure`, `reset!`, `ensure_configured!`)
|
|
9
|
+
- [**Errors**](errors.md) -- Error hierarchy: `Error`, `GenerationError`, `ValidationError`, `SecurityError`
|
|
10
|
+
|
|
11
|
+
## Quick Reference
|
|
12
|
+
|
|
13
|
+
### Instance Methods (from `include SelfAgency`)
|
|
14
|
+
|
|
15
|
+
| Method | Returns | Description |
|
|
16
|
+
|--------|---------|-------------|
|
|
17
|
+
| `_(description, scope:)` | `Array<Symbol>` | Generate and install methods from a description |
|
|
18
|
+
| `self_agency_generate(description, scope:)` | `Array<Symbol>` | Alias for `_()` |
|
|
19
|
+
| `_source_for(method_name)` | `String` or `nil` | Retrieve source code for a method |
|
|
20
|
+
| `_save!(as:, path:)` | `String` | Save generated methods as a subclass file |
|
|
21
|
+
| `on_method_generated(name, scope, code)` | - | Lifecycle hook (override in your class) |
|
|
22
|
+
|
|
23
|
+
### Class Methods (from `extend ClassMethods`)
|
|
24
|
+
|
|
25
|
+
| Method | Returns | Description |
|
|
26
|
+
|--------|---------|-------------|
|
|
27
|
+
| `_source_for(method_name)` | `String` or `nil` | Retrieve source code at the class level |
|
|
28
|
+
| `_source_versions_for(method_name)` | `Array<Hash>` | Version history for a generated method |
|
|
29
|
+
|
|
30
|
+
### Module-Level Methods
|
|
31
|
+
|
|
32
|
+
| Method | Returns | Description |
|
|
33
|
+
|--------|---------|-------------|
|
|
34
|
+
| `SelfAgency.configure { \|c\| ... }` | `Configuration` | Configure the gem (required) |
|
|
35
|
+
| `SelfAgency.configuration` | `Configuration` | Access current configuration |
|
|
36
|
+
| `SelfAgency.reset!` | - | Restore defaults |
|
|
37
|
+
| `SelfAgency.ensure_configured!` | - | Raise if not configured |
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
# SelfAgency Module
|
|
2
|
+
|
|
3
|
+
The main mixin module. Include it in any class to enable LLM-powered method generation.
|
|
4
|
+
|
|
5
|
+
```ruby
|
|
6
|
+
class MyClass
|
|
7
|
+
include SelfAgency
|
|
8
|
+
end
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Including `SelfAgency` adds instance methods to the class and extends it with `SelfAgency::ClassMethods`.
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## Instance Methods
|
|
16
|
+
|
|
17
|
+
### `_(description, scope: :instance)`
|
|
18
|
+
|
|
19
|
+
Generate and install one or more methods from a natural language description.
|
|
20
|
+
|
|
21
|
+
**Parameters:**
|
|
22
|
+
|
|
23
|
+
| Name | Type | Default | Description |
|
|
24
|
+
|------|------|---------|-------------|
|
|
25
|
+
| `description` | `String` | *(required)* | Natural language description of the method(s) |
|
|
26
|
+
| `scope` | `Symbol` | `:instance` | One of `:instance`, `:singleton`, `:class` |
|
|
27
|
+
|
|
28
|
+
**Returns:** `Array<Symbol>` -- names of the newly defined methods.
|
|
29
|
+
|
|
30
|
+
**Raises:**
|
|
31
|
+
|
|
32
|
+
| Exception | Condition |
|
|
33
|
+
|-----------|-----------|
|
|
34
|
+
| `SelfAgency::Error` | `SelfAgency.configure` has not been called |
|
|
35
|
+
| `SelfAgency::GenerationError` | LLM returned `nil` at shape or generate stage |
|
|
36
|
+
| `SelfAgency::ValidationError` | Generated code is empty, malformed, or has syntax errors |
|
|
37
|
+
| `SelfAgency::SecurityError` | Generated code contains a dangerous pattern |
|
|
38
|
+
|
|
39
|
+
**Example:**
|
|
40
|
+
|
|
41
|
+
```ruby
|
|
42
|
+
names = obj._("an instance method to add two integers")
|
|
43
|
+
#=> [:add]
|
|
44
|
+
|
|
45
|
+
names = obj._("a class method named 'self.ping' that returns 'pong'", scope: :class)
|
|
46
|
+
#=> [:ping]
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
---
|
|
50
|
+
|
|
51
|
+
### `self_agency_generate(description, scope: :instance)`
|
|
52
|
+
|
|
53
|
+
Alias for `_()`. Provides a named alternative when `_` conflicts with other conventions (e.g., i18n):
|
|
54
|
+
|
|
55
|
+
```ruby
|
|
56
|
+
names = obj.self_agency_generate("a method to add two integers")
|
|
57
|
+
#=> [:add]
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
---
|
|
61
|
+
|
|
62
|
+
### `_source_for(method_name)`
|
|
63
|
+
|
|
64
|
+
Return the source code for a method, or `nil` if unavailable.
|
|
65
|
+
|
|
66
|
+
For LLM-generated methods, returns the code with the original description as a comment header. For file-defined methods, falls back to the `method_source` gem.
|
|
67
|
+
|
|
68
|
+
**Parameters:**
|
|
69
|
+
|
|
70
|
+
| Name | Type | Description |
|
|
71
|
+
|------|------|-------------|
|
|
72
|
+
| `method_name` | `Symbol` or `String` | The method to look up |
|
|
73
|
+
|
|
74
|
+
**Returns:** `String` or `nil`.
|
|
75
|
+
|
|
76
|
+
**Example:**
|
|
77
|
+
|
|
78
|
+
```ruby
|
|
79
|
+
puts obj._source_for(:add)
|
|
80
|
+
# >> # an instance method to add two integers
|
|
81
|
+
# >> def add(a, b)
|
|
82
|
+
# >> a + b
|
|
83
|
+
# >> end
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
---
|
|
87
|
+
|
|
88
|
+
### `_save!(as:, path: nil)`
|
|
89
|
+
|
|
90
|
+
Save the object's generated methods as a subclass in a Ruby source file.
|
|
91
|
+
|
|
92
|
+
**Parameters:**
|
|
93
|
+
|
|
94
|
+
| Name | Type | Default | Description |
|
|
95
|
+
|------|------|---------|-------------|
|
|
96
|
+
| `as` | `String` or `Symbol` | *(required)* | Subclass name (snake_case converted to CamelCase) |
|
|
97
|
+
| `path` | `String` or `nil` | `nil` | Output file path (defaults to snake_cased name + `.rb`) |
|
|
98
|
+
|
|
99
|
+
**Returns:** `String` -- the file path written to.
|
|
100
|
+
|
|
101
|
+
**Raises:**
|
|
102
|
+
|
|
103
|
+
| Exception | Condition |
|
|
104
|
+
|-----------|-----------|
|
|
105
|
+
| `ArgumentError` | `as:` is not a String or Symbol |
|
|
106
|
+
| `SelfAgency::Error` | No generated methods to save |
|
|
107
|
+
| `SelfAgency::Error` | Parent class is anonymous |
|
|
108
|
+
|
|
109
|
+
**Example:**
|
|
110
|
+
|
|
111
|
+
```ruby
|
|
112
|
+
path = obj._save!(as: :calculator)
|
|
113
|
+
#=> "calculator.rb"
|
|
114
|
+
|
|
115
|
+
path = obj._save!(as: :calculator, path: "lib/calculator.rb")
|
|
116
|
+
#=> "lib/calculator.rb"
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
---
|
|
120
|
+
|
|
121
|
+
### `on_method_generated(method_name, scope, code)`
|
|
122
|
+
|
|
123
|
+
Lifecycle hook called once per generated method. Override in your class to persist or log generated methods.
|
|
124
|
+
|
|
125
|
+
**Parameters:**
|
|
126
|
+
|
|
127
|
+
| Name | Type | Description |
|
|
128
|
+
|------|------|-------------|
|
|
129
|
+
| `method_name` | `Symbol` | Name of the generated method |
|
|
130
|
+
| `scope` | `Symbol` | `:instance`, `:singleton`, or `:class` |
|
|
131
|
+
| `code` | `String` | The generated source code |
|
|
132
|
+
|
|
133
|
+
**Default behavior:** No-op.
|
|
134
|
+
|
|
135
|
+
**Example:**
|
|
136
|
+
|
|
137
|
+
```ruby
|
|
138
|
+
def on_method_generated(method_name, scope, code)
|
|
139
|
+
File.write("generated/#{method_name}.rb", code)
|
|
140
|
+
end
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
---
|
|
144
|
+
|
|
145
|
+
## Class Methods (via ClassMethods)
|
|
146
|
+
|
|
147
|
+
### `_source_for(method_name)`
|
|
148
|
+
|
|
149
|
+
Class-level version of `_source_for`. Works identically to the instance method but is called on the class.
|
|
150
|
+
|
|
151
|
+
**Parameters:**
|
|
152
|
+
|
|
153
|
+
| Name | Type | Description |
|
|
154
|
+
|------|------|-------------|
|
|
155
|
+
| `method_name` | `Symbol` or `String` | The method to look up |
|
|
156
|
+
|
|
157
|
+
**Returns:** `String` or `nil`.
|
|
158
|
+
|
|
159
|
+
**Example:**
|
|
160
|
+
|
|
161
|
+
```ruby
|
|
162
|
+
puts MyClass._source_for(:add)
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
---
|
|
166
|
+
|
|
167
|
+
### `_source_versions_for(method_name)`
|
|
168
|
+
|
|
169
|
+
Return the version history for a generated method. Each entry records the code, description, generating instance, and timestamp.
|
|
170
|
+
|
|
171
|
+
**Parameters:**
|
|
172
|
+
|
|
173
|
+
| Name | Type | Description |
|
|
174
|
+
|------|------|-------------|
|
|
175
|
+
| `method_name` | `Symbol` or `String` | The method to look up |
|
|
176
|
+
|
|
177
|
+
**Returns:** `Array<Hash>` -- each Hash contains:
|
|
178
|
+
|
|
179
|
+
| Key | Type | Description |
|
|
180
|
+
|-----|------|-------------|
|
|
181
|
+
| `:code` | `String` | The generated source code |
|
|
182
|
+
| `:description` | `String` | The description passed to `_()` |
|
|
183
|
+
| `:instance_id` | `Integer` | `object_id` of the instance that generated it |
|
|
184
|
+
| `:at` | `Time` | When the method was generated |
|
|
185
|
+
|
|
186
|
+
Returns an empty array if no versions exist.
|
|
187
|
+
|
|
188
|
+
**Example:**
|
|
189
|
+
|
|
190
|
+
```ruby
|
|
191
|
+
obj._("add two integers")
|
|
192
|
+
obj._("add two integers, raise ArgumentError if either is negative")
|
|
193
|
+
|
|
194
|
+
versions = MyClass._source_versions_for(:add)
|
|
195
|
+
versions.size #=> 2
|
|
196
|
+
versions.last[:at] #=> 2025-01-31 12:34:56 -0500
|
|
197
|
+
versions.last[:description] #=> "add two integers, raise ArgumentError if either is negative"
|
|
198
|
+
```
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
# Architecture Overview
|
|
2
|
+
|
|
3
|
+
SelfAgency uses a two-stage LLM pipeline with multi-layer security to generate and install methods at runtime.
|
|
4
|
+
|
|
5
|
+
## Pipeline
|
|
6
|
+
|
|
7
|
+
```mermaid
|
|
8
|
+
flowchart TD
|
|
9
|
+
A["User calls _('description')"] --> B[Acquire per-class mutex]
|
|
10
|
+
B --> C[ensure_configured!]
|
|
11
|
+
C --> D[Shape Stage]
|
|
12
|
+
D --> E{Shaped spec nil?}
|
|
13
|
+
E -->|Yes| F[Raise GenerationError]
|
|
14
|
+
E -->|No| G[Generate Stage]
|
|
15
|
+
G --> H{Raw code nil?}
|
|
16
|
+
H -->|Yes| F
|
|
17
|
+
H -->|No| I[Sanitize]
|
|
18
|
+
I --> J[Validate]
|
|
19
|
+
J --> K{Valid?}
|
|
20
|
+
K -->|No| L{Retries left?}
|
|
21
|
+
L -->|Yes| M[Feed error + code back to LLM]
|
|
22
|
+
M --> G
|
|
23
|
+
L -->|No| N[Raise ValidationError or SecurityError]
|
|
24
|
+
K -->|Yes| O[Sandbox Eval]
|
|
25
|
+
O --> P[Split Methods]
|
|
26
|
+
P --> Q[Store Source + Version History]
|
|
27
|
+
Q --> R[Fire on_method_generated Hook]
|
|
28
|
+
R --> S["Return Array<Symbol>"]
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Stage 1: Shape
|
|
32
|
+
|
|
33
|
+
The shape stage rewrites a casual language description into a precise Ruby method specification. It uses ERB templates from the `shape/` directory.
|
|
34
|
+
|
|
35
|
+
The LLM receives class context:
|
|
36
|
+
|
|
37
|
+
- **Class name** -- e.g., `Calculator`
|
|
38
|
+
- **Instance variables** -- e.g., `@data, @name`
|
|
39
|
+
- **Public methods** -- e.g., `add, subtract, mean`
|
|
40
|
+
- **Scope instruction** -- e.g., "This will be an instance method available on all instances of the class."
|
|
41
|
+
|
|
42
|
+
The shape stage does **not** produce code. It produces a refined natural language specification that the generate stage can work with reliably.
|
|
43
|
+
|
|
44
|
+
## Stage 2: Generate
|
|
45
|
+
|
|
46
|
+
The generate stage takes the shaped specification and produces a `def...end` block. It uses templates from the `generate/` directory.
|
|
47
|
+
|
|
48
|
+
The LLM receives the same class context plus the shaped specification from stage 1.
|
|
49
|
+
|
|
50
|
+
If validation or security checks fail, the generate stage retries up to `generation_retries` times (default: 3). On each retry, the previous error message and failed code are injected into the generate template via `previous_error` and `previous_code` variables, allowing the LLM to self-correct.
|
|
51
|
+
|
|
52
|
+
## Post-Processing
|
|
53
|
+
|
|
54
|
+
After generation, the raw LLM output goes through three steps:
|
|
55
|
+
|
|
56
|
+
### Sanitize
|
|
57
|
+
|
|
58
|
+
Strips artifacts from the LLM response:
|
|
59
|
+
|
|
60
|
+
- Markdown code fences (` ```ruby ... ``` `)
|
|
61
|
+
- `<think>` blocks (used by some models for chain-of-thought reasoning)
|
|
62
|
+
- Leading/trailing whitespace
|
|
63
|
+
|
|
64
|
+
### Validate
|
|
65
|
+
|
|
66
|
+
Four checks run in sequence:
|
|
67
|
+
|
|
68
|
+
1. **Non-empty** -- Code must not be blank
|
|
69
|
+
2. **Structure** -- Must contain at least one `def...end` block
|
|
70
|
+
3. **Security** -- Must not match any `DANGEROUS_PATTERNS`
|
|
71
|
+
4. **Syntax** -- Must compile via `RubyVM::InstructionSequence.compile`
|
|
72
|
+
|
|
73
|
+
### Sandbox Eval
|
|
74
|
+
|
|
75
|
+
The validated code is evaluated inside a sandboxed module that includes `SelfAgency::Sandbox`. This module shadows dangerous Kernel methods, placing them ahead of Kernel in Ruby's method resolution order (MRO).
|
|
76
|
+
|
|
77
|
+
Sandbox modules are **cached per scope** to prevent ancestor chain accumulation across multiple `_()` calls:
|
|
78
|
+
|
|
79
|
+
| Scope | Prepend Target | Cache Level |
|
|
80
|
+
|-------|---------------|-------------|
|
|
81
|
+
| `:instance` | `self.class` | Per class |
|
|
82
|
+
| `:singleton` | `singleton_class` | Per instance |
|
|
83
|
+
| `:class` | `self.class.singleton_class` | Per class |
|
|
84
|
+
|
|
85
|
+
On the first `_()` call for a given scope, a new anonymous module is created, prepended, and cached. Subsequent calls reuse the same module, defining new methods into it rather than creating additional anonymous modules.
|
|
86
|
+
|
|
87
|
+
## Module Structure
|
|
88
|
+
|
|
89
|
+
```mermaid
|
|
90
|
+
classDiagram
|
|
91
|
+
class SelfAgency {
|
|
92
|
+
+_(description, scope) Array~Symbol~
|
|
93
|
+
+self_agency_generate(description, scope) Array~Symbol~
|
|
94
|
+
+_source_for(method_name) String?
|
|
95
|
+
+_save!(as, path) String
|
|
96
|
+
+on_method_generated(name, scope, code)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
class ClassMethods {
|
|
100
|
+
+_source_for(method_name) String?
|
|
101
|
+
+_source_versions_for(method_name) Array~Hash~
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
class Configuration {
|
|
105
|
+
+provider Symbol
|
|
106
|
+
+model String
|
|
107
|
+
+api_base String
|
|
108
|
+
+request_timeout Integer
|
|
109
|
+
+max_retries Integer
|
|
110
|
+
+retry_interval Float
|
|
111
|
+
+template_directory String
|
|
112
|
+
+generation_retries Integer
|
|
113
|
+
+logger Proc/Logger/nil
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
class Sandbox {
|
|
117
|
+
-system(*) raises SecurityError
|
|
118
|
+
-exec(*) raises SecurityError
|
|
119
|
+
-spawn(*) raises SecurityError
|
|
120
|
+
-fork(*) raises SecurityError
|
|
121
|
+
-backticks(*) raises SecurityError
|
|
122
|
+
-open(*) raises SecurityError
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
class Validator {
|
|
126
|
+
+DANGEROUS_PATTERNS Regexp
|
|
127
|
+
-self_agency_sanitize(raw) String
|
|
128
|
+
-self_agency_validate!(code)
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
class Generator {
|
|
132
|
+
-self_agency_ask_with_template(name, **vars) String?
|
|
133
|
+
-self_agency_shape(prompt, scope) String?
|
|
134
|
+
-self_agency_generation_vars() Hash
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
class Saver {
|
|
138
|
+
-self_agency_to_class_name(value) String
|
|
139
|
+
-self_agency_to_snake_case(name) String
|
|
140
|
+
-self_agency_relative_require(output, source) String
|
|
141
|
+
-self_agency_build_subclass_source(...) String
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
SelfAgency --> ClassMethods : extends including class
|
|
145
|
+
SelfAgency --> Configuration : uses
|
|
146
|
+
SelfAgency --> Sandbox : includes in eval module
|
|
147
|
+
SelfAgency --> Validator : validates code
|
|
148
|
+
SelfAgency --> Generator : calls LLM
|
|
149
|
+
SelfAgency --> Saver : persists methods
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
## Thread Safety
|
|
153
|
+
|
|
154
|
+
SelfAgency uses two mutexes to ensure thread-safe operation:
|
|
155
|
+
|
|
156
|
+
- **`CONFIG_MUTEX`** (module-level) -- Serializes `SelfAgency.configure` and `SelfAgency.reset!` calls so that concurrent configuration changes do not interleave.
|
|
157
|
+
- **Per-class mutex** (`@self_agency_mutex`) -- Initialized when a class includes `SelfAgency`. Serializes the entire `_()` pipeline per class so that concurrent method generation calls do not interfere with each other.
|
|
158
|
+
|
|
159
|
+
The per-class mutex wraps the full pipeline: shape, generate, validate (with retries), eval, source storage, and lifecycle hook. This means only one thread can generate methods for a given class at a time, but different classes can generate concurrently.
|
|
160
|
+
|
|
161
|
+
## File Layout
|
|
162
|
+
|
|
163
|
+
```
|
|
164
|
+
lib/
|
|
165
|
+
self_agency.rb # Main module, public API, eval logic
|
|
166
|
+
self_agency/
|
|
167
|
+
version.rb # VERSION constant
|
|
168
|
+
errors.rb # Error hierarchy
|
|
169
|
+
configuration.rb # Configuration class and singleton methods
|
|
170
|
+
sandbox.rb # Runtime sandbox module
|
|
171
|
+
validator.rb # DANGEROUS_PATTERNS, sanitize, validate!
|
|
172
|
+
generator.rb # LLM communication and prompt shaping
|
|
173
|
+
saver.rb # _save! helpers
|
|
174
|
+
prompts/
|
|
175
|
+
shape/
|
|
176
|
+
system.txt.erb # Shape stage system prompt
|
|
177
|
+
user.txt.erb # Shape stage user prompt
|
|
178
|
+
generate/
|
|
179
|
+
system.txt.erb # Generate stage system prompt
|
|
180
|
+
user.txt.erb # Generate stage user prompt
|
|
181
|
+
```
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
# Security
|
|
2
|
+
|
|
3
|
+
SelfAgency employs a two-layer security model to prevent generated code from performing dangerous operations. Both layers must pass before any code is installed.
|
|
4
|
+
|
|
5
|
+
## Layer 1: Static Analysis
|
|
6
|
+
|
|
7
|
+
Before code is evaluated, it is checked against `DANGEROUS_PATTERNS`, a compiled regular expression that matches known dangerous constructs:
|
|
8
|
+
|
|
9
|
+
| Pattern | What It Catches |
|
|
10
|
+
|---------|----------------|
|
|
11
|
+
| `\b(system\|exec\|spawn\|fork\|abort\|exit)\b` | Process execution and termination |
|
|
12
|
+
| `` `[^`]*` `` | Backtick shell execution |
|
|
13
|
+
| `%x\{`, `%x\[`, `%x\(` | `%x` shell execution syntax |
|
|
14
|
+
| `\bFile\.\b` | File system access |
|
|
15
|
+
| `\bIO\.\b` | I/O operations |
|
|
16
|
+
| `\bKernel\.\b` | Direct Kernel calls |
|
|
17
|
+
| `\bOpen3\.\b` | Advanced process spawning |
|
|
18
|
+
| `\bProcess\.\b` | Process management |
|
|
19
|
+
| `\brequire\b` | Loading external code |
|
|
20
|
+
| `\bload\b` | Loading external code |
|
|
21
|
+
| `\b__send__\b` | Method dispatch bypass |
|
|
22
|
+
| `\beval\b` | Dynamic code evaluation |
|
|
23
|
+
| `\bsend\b` | Method dispatch (`send`) |
|
|
24
|
+
| `\bpublic_send\b` | Method dispatch (`public_send`) |
|
|
25
|
+
| `\bmethod\s*\(` | Method object retrieval |
|
|
26
|
+
| `\bconst_get\b` | Constant lookup bypass |
|
|
27
|
+
| `\bclass_eval\b` | Class-level eval |
|
|
28
|
+
| `\bmodule_eval\b` | Module-level eval |
|
|
29
|
+
| `\binstance_eval\b` | Instance-level eval |
|
|
30
|
+
| `\binstance_variable_set\b` | Direct ivar write |
|
|
31
|
+
| `\binstance_variable_get\b` | Direct ivar read |
|
|
32
|
+
| `\bdefine_method\b` | Dynamic method definition |
|
|
33
|
+
| `\bBinding\b` | Binding access |
|
|
34
|
+
| `\bBasicObject\b` | BasicObject escape hatch |
|
|
35
|
+
| `\bremove_method\b` | Method removal |
|
|
36
|
+
| `\bundef_method\b` | Method undefinition |
|
|
37
|
+
|
|
38
|
+
If any pattern matches, a `SelfAgency::SecurityError` is raised and the code is **not evaluated**.
|
|
39
|
+
|
|
40
|
+
```ruby
|
|
41
|
+
# These all raise SelfAgency::SecurityError:
|
|
42
|
+
"def hack\n system('ls')\nend"
|
|
43
|
+
"def hack\n File.read('/etc/passwd')\nend"
|
|
44
|
+
"def hack\n eval('1+1')\nend"
|
|
45
|
+
"def hack\n require 'socket'\nend"
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Layer 2: Runtime Sandbox
|
|
49
|
+
|
|
50
|
+
Even if static analysis were bypassed, the runtime sandbox provides a second line of defense. Every generated method is evaluated inside an anonymous module that includes `SelfAgency::Sandbox`:
|
|
51
|
+
|
|
52
|
+
```ruby
|
|
53
|
+
module SelfAgency::Sandbox
|
|
54
|
+
private
|
|
55
|
+
|
|
56
|
+
def system(*) = raise(::SecurityError, "system() blocked by SelfAgency sandbox")
|
|
57
|
+
def exec(*) = raise(::SecurityError, "exec() blocked by SelfAgency sandbox")
|
|
58
|
+
def spawn(*) = raise(::SecurityError, "spawn() blocked by SelfAgency sandbox")
|
|
59
|
+
def fork(*) = raise(::SecurityError, "fork() blocked by SelfAgency sandbox")
|
|
60
|
+
def `(*) = raise(::SecurityError, "backtick execution blocked by SelfAgency sandbox")
|
|
61
|
+
def open(*) = raise(::SecurityError, "open() blocked by SelfAgency sandbox")
|
|
62
|
+
end
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Because the sandbox module is included in the anonymous module that wraps the generated code, its methods appear **ahead of Kernel** in Ruby's method resolution order (MRO). Any call to `system`, `exec`, `spawn`, `fork`, backticks, or `open` from within a generated method raises `::SecurityError` at runtime.
|
|
66
|
+
|
|
67
|
+
## Validation Pipeline
|
|
68
|
+
|
|
69
|
+
The full validation sequence runs in order. The first failure stops evaluation:
|
|
70
|
+
|
|
71
|
+
```mermaid
|
|
72
|
+
flowchart LR
|
|
73
|
+
A[Raw LLM Output] --> B[Sanitize]
|
|
74
|
+
B --> C{Empty?}
|
|
75
|
+
C -->|Yes| D[ValidationError]
|
|
76
|
+
C -->|No| E{"Has def...end?"}
|
|
77
|
+
E -->|No| D
|
|
78
|
+
E -->|Yes| F{Dangerous pattern?}
|
|
79
|
+
F -->|Yes| G[SecurityError]
|
|
80
|
+
F -->|No| H{Syntax valid?}
|
|
81
|
+
H -->|No| D
|
|
82
|
+
H -->|Yes| I[Sandbox Eval]
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
1. **Sanitize** -- Strip markdown fences, `<think>` blocks, whitespace
|
|
86
|
+
2. **Empty check** -- Raise `ValidationError` if code is blank
|
|
87
|
+
3. **Structure check** -- Raise `ValidationError` if no `def...end` block found
|
|
88
|
+
4. **Pattern check** -- Raise `SecurityError` if `DANGEROUS_PATTERNS` matches
|
|
89
|
+
5. **Syntax check** -- Raise `ValidationError` if `RubyVM::InstructionSequence.compile` fails
|
|
90
|
+
6. **Sandbox eval** -- Evaluate inside sandboxed anonymous module
|
|
91
|
+
|
|
92
|
+
## Limitations
|
|
93
|
+
|
|
94
|
+
The security model is designed for defense-in-depth against accidental or LLM-hallucinated dangerous code. It is **not** a full sandboxing solution:
|
|
95
|
+
|
|
96
|
+
- Static patterns can potentially be bypassed through creative obfuscation
|
|
97
|
+
- The runtime sandbox only shadows six specific Kernel methods
|
|
98
|
+
- Generated code has access to the full Ruby standard library (except blocked methods)
|
|
99
|
+
- Network access (e.g., `Net::HTTP`) is not blocked by default
|
|
100
|
+
|
|
101
|
+
For production use, consider additional controls such as network-level restrictions, process isolation, or reviewing generated code before deployment (see [`_save!`](../guide/saving-methods.md)).
|
|
Binary file
|
|
Binary file
|