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.
Files changed (78) hide show
  1. checksums.yaml +7 -0
  2. data/.envrc +1 -0
  3. data/.github/workflows/deploy-github-pages.yml +40 -0
  4. data/.irbrc +22 -0
  5. data/CHANGELOG.md +5 -0
  6. data/COMMITS.md +196 -0
  7. data/LICENSE.txt +21 -0
  8. data/README.md +177 -0
  9. data/Rakefile +8 -0
  10. data/docs/api/configuration.md +85 -0
  11. data/docs/api/errors.md +166 -0
  12. data/docs/api/index.md +37 -0
  13. data/docs/api/self-agency-module.md +198 -0
  14. data/docs/architecture/overview.md +181 -0
  15. data/docs/architecture/security.md +101 -0
  16. data/docs/assets/images/self_agency.gif +0 -0
  17. data/docs/assets/images/self_agency.mp4 +0 -0
  18. data/docs/development/contributing.md +45 -0
  19. data/docs/development/setup.md +81 -0
  20. data/docs/development/testing.md +70 -0
  21. data/docs/examples/autonomous-robots.md +109 -0
  22. data/docs/examples/basic-examples.md +237 -0
  23. data/docs/examples/collaborative-robots.md +98 -0
  24. data/docs/examples/full-workflow.md +100 -0
  25. data/docs/examples/index.md +36 -0
  26. data/docs/getting-started/installation.md +71 -0
  27. data/docs/getting-started/quick-start.md +94 -0
  28. data/docs/guide/configuration.md +113 -0
  29. data/docs/guide/generating-methods.md +146 -0
  30. data/docs/guide/how-to-use.md +144 -0
  31. data/docs/guide/lifecycle-hooks.md +86 -0
  32. data/docs/guide/prompt-templates.md +189 -0
  33. data/docs/guide/saving-methods.md +84 -0
  34. data/docs/guide/scopes.md +74 -0
  35. data/docs/guide/source-inspection.md +96 -0
  36. data/docs/index.md +77 -0
  37. data/examples/01_basic_usage.rb +27 -0
  38. data/examples/02_multiple_methods.rb +43 -0
  39. data/examples/03_scopes.rb +40 -0
  40. data/examples/04_source_inspection.rb +46 -0
  41. data/examples/05_lifecycle_hook.rb +55 -0
  42. data/examples/06_configuration.rb +97 -0
  43. data/examples/07_error_handling.rb +103 -0
  44. data/examples/08_class_context.rb +64 -0
  45. data/examples/09_method_override.rb +52 -0
  46. data/examples/10_full_workflow.rb +118 -0
  47. data/examples/11_collaborative_robots/atlas.rb +31 -0
  48. data/examples/11_collaborative_robots/echo.rb +30 -0
  49. data/examples/11_collaborative_robots/main.rb +190 -0
  50. data/examples/11_collaborative_robots/nova.rb +71 -0
  51. data/examples/11_collaborative_robots/robot.rb +119 -0
  52. data/examples/12_autonomous_robots/analyst.rb +193 -0
  53. data/examples/12_autonomous_robots/collector.rb +78 -0
  54. data/examples/12_autonomous_robots/main.rb +166 -0
  55. data/examples/12_autonomous_robots/planner.rb +125 -0
  56. data/examples/12_autonomous_robots/robot.rb +284 -0
  57. data/examples/generated/from_range_class.rb +3 -0
  58. data/examples/generated/mean_instance.rb +4 -0
  59. data/examples/generated/median_instance.rb +15 -0
  60. data/examples/generated/report_singleton.rb +3 -0
  61. data/examples/generated/standard_deviation_instance.rb +8 -0
  62. data/examples/lib/message_bus.rb +57 -0
  63. data/examples/lib/setup.rb +8 -0
  64. data/lib/self_agency/configuration.rb +76 -0
  65. data/lib/self_agency/errors.rb +35 -0
  66. data/lib/self_agency/generator.rb +47 -0
  67. data/lib/self_agency/prompts/generate/system.txt.erb +15 -0
  68. data/lib/self_agency/prompts/generate/user.txt.erb +13 -0
  69. data/lib/self_agency/prompts/shape/system.txt.erb +26 -0
  70. data/lib/self_agency/prompts/shape/user.txt.erb +10 -0
  71. data/lib/self_agency/sandbox.rb +17 -0
  72. data/lib/self_agency/saver.rb +62 -0
  73. data/lib/self_agency/validator.rb +64 -0
  74. data/lib/self_agency/version.rb +5 -0
  75. data/lib/self_agency.rb +315 -0
  76. data/mkdocs.yml +156 -0
  77. data/sig/self_agency.rbs +4 -0
  78. metadata +163 -0
@@ -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