ruby-mana 0.5.1 → 0.5.7

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.
data/lib/mana/mixin.rb CHANGED
@@ -11,8 +11,8 @@ module Mana
11
11
  module ClassMethods
12
12
  # Mark a method for LLM compilation.
13
13
  # Usage:
14
- # mana def fizzbuzz(n)
15
- # ~"return FizzBuzz array from 1 to n"
14
+ # mana def fibonacci(n)
15
+ # ~"return an array of the first n Fibonacci numbers"
16
16
  # end
17
17
  def mana(method_name)
18
18
  Mana::Compiler.compile(self, method_name)
data/lib/mana/mock.rb CHANGED
@@ -1,23 +1,39 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Mana
4
+ # Test double for LLM calls. Stubs are matched against prompt text:
5
+ # Regexp patterns use match?, String patterns use include?.
6
+ #
7
+ # Usage:
8
+ # Mana.mock do
9
+ # prompt(/average/, result: 3.0)
10
+ # ~"compute average of <numbers> and store in <result>"
11
+ # end
12
+ #
13
+ # Thread-local — safe for parallel test execution.
4
14
  class Mock
5
15
  Stub = Struct.new(:pattern, :values, :block, keyword_init: true)
6
16
 
7
17
  attr_reader :stubs
8
18
 
19
+ # Initialize with an empty stub list
9
20
  def initialize
10
21
  @stubs = []
11
22
  end
12
23
 
24
+ # Register a stub: when a prompt matches `pattern`, return `values` or call `block`
13
25
  def prompt(pattern, **values, &block)
14
26
  @stubs << Stub.new(pattern: pattern, values: values, block: block)
15
27
  end
16
28
 
29
+ # Find the first stub that matches the given prompt text.
30
+ # Regexp patterns use match?, String patterns use include?.
17
31
  def match(prompt_text)
18
32
  @stubs.find do |stub|
19
33
  case stub.pattern
34
+ # Regex: full pattern match
20
35
  when Regexp then prompt_text.match?(stub.pattern)
36
+ # String: substring match
21
37
  when String then prompt_text.include?(stub.pattern)
22
38
  end
23
39
  end
@@ -25,29 +41,53 @@ module Mana
25
41
  end
26
42
 
27
43
  class << self
44
+ # Run a block with a mock active. Stubs defined inside are scoped to this block.
45
+ # The block is instance_eval'd on the Mock so `prompt` is available as DSL.
28
46
  def mock(&block)
29
47
  old_mock = Thread.current[:mana_mock]
30
48
  m = Mock.new
31
49
  Thread.current[:mana_mock] = m
32
50
  m.instance_eval(&block)
51
+ # Always restore the previous mock (supports nesting)
33
52
  ensure
34
53
  Thread.current[:mana_mock] = old_mock
35
54
  end
36
55
 
56
+ # Enable mock mode (for use with before/after hooks in tests)
37
57
  def mock!
38
58
  Thread.current[:mana_mock] = Mock.new
39
59
  end
40
60
 
61
+ # Disable mock mode and restore normal LLM calls
41
62
  def unmock!
42
63
  Thread.current[:mana_mock] = nil
43
64
  end
44
65
 
66
+ # Check if mock mode is currently active on this thread
45
67
  def mock_active?
46
68
  !Thread.current[:mana_mock].nil?
47
69
  end
48
70
 
71
+ # Returns the current thread's Mock instance, or nil if not in mock mode
49
72
  def current_mock
50
73
  Thread.current[:mana_mock]
51
74
  end
52
75
  end
76
+
77
+ # RSpec helper — include in your test suite for automatic mock setup.
78
+ # RSpec.configure { |c| c.include Mana::TestHelpers }
79
+ module TestHelpers
80
+ # Auto-enable mock mode before each test, disable after
81
+ def self.included(base)
82
+ base.before { Mana.mock! }
83
+ base.after { Mana.unmock! }
84
+ end
85
+
86
+ # Convenience method to register a stub within the current mock context
87
+ def mock_prompt(pattern, **values, &block)
88
+ raise Mana::MockError, "Mana mock mode not active. Call Mana.mock! first" unless Mana.mock_active?
89
+
90
+ Mana.current_mock.prompt(pattern, **values, &block)
91
+ end
92
+ end
53
93
  end
@@ -0,0 +1,195 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "set"
4
+
5
+ module Mana
6
+ # Configurable security policy for LLM tool calls.
7
+ #
8
+ # Five levels (higher = more permissions, each includes all below):
9
+ # 0 :sandbox — variables and user functions only
10
+ # 1 :strict — + safe stdlib (Time, Date, Math)
11
+ # 2 :standard — + read filesystem (File.read, Dir.glob) [default]
12
+ # 3 :permissive — + write files, network, require
13
+ # 4 :danger — no restrictions
14
+ #
15
+ # Usage:
16
+ # Mana.configure { |c| c.security = :standard }
17
+ # Mana.configure { |c| c.security = 2 }
18
+ # Mana.configure do |c|
19
+ # c.security = :strict
20
+ # c.security.allow_receiver "File", only: %w[read exist?]
21
+ # end
22
+ class SecurityPolicy
23
+ LEVELS = { sandbox: 0, strict: 1, standard: 2, permissive: 3, danger: 4 }.freeze
24
+
25
+ # Methods blocked at each level. Higher levels remove restrictions.
26
+ PRESETS = {
27
+ sandbox: {
28
+ blocked_methods: %w[
29
+ methods singleton_methods private_methods protected_methods public_methods
30
+ instance_variables instance_variable_get instance_variable_set remove_instance_variable
31
+ local_variables global_variables
32
+ send __send__ public_send eval instance_eval instance_exec class_eval module_eval
33
+ system exec fork spawn ` require require_relative load
34
+ exit exit! abort at_exit
35
+ ],
36
+ blocked_receivers: :all
37
+ },
38
+ strict: {
39
+ blocked_methods: %w[
40
+ methods singleton_methods private_methods protected_methods public_methods
41
+ instance_variables instance_variable_get instance_variable_set remove_instance_variable
42
+ local_variables global_variables
43
+ send __send__ public_send eval instance_eval instance_exec class_eval module_eval
44
+ system exec fork spawn ` require require_relative load
45
+ exit exit! abort at_exit
46
+ ],
47
+ blocked_receivers: {
48
+ "File" => :all, "Dir" => :all, "IO" => :all,
49
+ "Kernel" => :all, "Process" => :all, "ObjectSpace" => :all, "ENV" => :all
50
+ }
51
+ },
52
+ standard: {
53
+ blocked_methods: %w[
54
+ methods singleton_methods private_methods protected_methods public_methods
55
+ instance_variables instance_variable_get instance_variable_set remove_instance_variable
56
+ local_variables global_variables
57
+ send __send__ public_send eval instance_eval instance_exec class_eval module_eval
58
+ system exec fork spawn `
59
+ exit exit! abort at_exit
60
+ require require_relative load
61
+ ],
62
+ blocked_receivers: {
63
+ "File" => Set.new(%w[delete write open chmod chown rename unlink]),
64
+ "Dir" => Set.new(%w[delete rmdir mkdir chdir]),
65
+ "IO" => :all, "Kernel" => :all, "Process" => :all,
66
+ "ObjectSpace" => :all, "ENV" => :all
67
+ }
68
+ },
69
+ permissive: {
70
+ blocked_methods: %w[
71
+ eval instance_eval instance_exec class_eval module_eval
72
+ system exec fork spawn `
73
+ exit exit! abort at_exit
74
+ ],
75
+ blocked_receivers: {
76
+ "ObjectSpace" => :all
77
+ }
78
+ },
79
+ danger: {
80
+ blocked_methods: [],
81
+ blocked_receivers: {}
82
+ }
83
+ }.freeze
84
+
85
+ attr_reader :preset
86
+
87
+ # Initialize security policy from a preset name (Symbol) or numeric level (Integer)
88
+ def initialize(preset = :strict)
89
+ # Convert numeric level to its corresponding symbol name
90
+ preset = LEVELS.key(preset) if preset.is_a?(Integer)
91
+ raise ArgumentError, "unknown security level: #{preset.inspect}. Use: #{LEVELS.keys.join(', ')}" unless PRESETS.key?(preset)
92
+
93
+ @preset = preset
94
+ data = PRESETS[preset]
95
+ @blocked_methods = Set.new(data[:blocked_methods])
96
+
97
+ if data[:blocked_receivers] == :all
98
+ # Sandbox mode: block all receiver calls by default
99
+ @block_all_receivers = true
100
+ @blocked_receivers = {}
101
+ else
102
+ # Other modes: block only specific methods on specific receivers
103
+ @block_all_receivers = false
104
+ @blocked_receivers = data[:blocked_receivers].transform_values { |v|
105
+ v == :all ? :all : Set.new(v)
106
+ }
107
+ end
108
+
109
+ # Allowlist overrides added via allow_receiver(name, only: [...])
110
+ @allowed_overrides = {}
111
+ yield self if block_given?
112
+ end
113
+
114
+ # --- Mutators ---
115
+
116
+ def allow_method(name)
117
+ @blocked_methods.delete(name.to_s)
118
+ end
119
+
120
+ def block_method(name)
121
+ @blocked_methods.add(name.to_s)
122
+ end
123
+
124
+ # Allow a receiver's calls. With `only:`, allowlist specific methods;
125
+ # without it, fully unblock the receiver.
126
+ def allow_receiver(name, only: nil)
127
+ name = name.to_s
128
+ if only
129
+ # Allowlist only the specified methods (override takes priority over block rules)
130
+ @allowed_overrides[name] = Set.new(only.map(&:to_s))
131
+ else
132
+ # Fully unblock the receiver and remove any override
133
+ @blocked_receivers.delete(name)
134
+ @allowed_overrides.delete(name)
135
+ end
136
+ end
137
+
138
+ # Block a receiver's calls. With `only:`, block specific methods;
139
+ # without it, block all methods on the receiver.
140
+ def block_receiver(name, only: nil)
141
+ name = name.to_s
142
+ if only
143
+ existing = @blocked_receivers[name]
144
+ if existing == :all
145
+ # Already fully blocked — nothing to add
146
+ elsif existing.is_a?(Set)
147
+ # Merge new blocked methods into the existing set
148
+ existing.merge(only.map(&:to_s))
149
+ else
150
+ # First partial block rule for this receiver
151
+ @blocked_receivers[name] = Set.new(only.map(&:to_s))
152
+ end
153
+ else
154
+ # Block all methods on this receiver
155
+ @blocked_receivers[name] = :all
156
+ @allowed_overrides.delete(name)
157
+ end
158
+ end
159
+
160
+ # --- Queries ---
161
+
162
+ def method_blocked?(name)
163
+ @blocked_methods.include?(name.to_s)
164
+ end
165
+
166
+ # Check whether calling `method` on `receiver` is blocked by the policy
167
+ def receiver_call_blocked?(receiver, method)
168
+ # Danger mode has no restrictions at all
169
+ return false if @preset == :danger
170
+
171
+ r, m = receiver.to_s, method.to_s
172
+
173
+ # Sandbox mode: block everything unless explicitly allowlisted
174
+ if @block_all_receivers
175
+ return !(@allowed_overrides.key?(r) && @allowed_overrides[r].include?(m))
176
+ end
177
+
178
+ # Receiver not in the block list — allow
179
+ rule = @blocked_receivers[r]
180
+ return false if rule.nil?
181
+
182
+ # Allowlist override takes priority: if user explicitly allowed this method, pass
183
+ if @allowed_overrides.key?(r) && @allowed_overrides[r].include?(m)
184
+ return false
185
+ end
186
+
187
+ # Blocked if receiver is fully blocked (:all) or method is in the blocked set
188
+ rule == :all || rule.include?(m)
189
+ end
190
+
191
+ def level
192
+ LEVELS[@preset]
193
+ end
194
+ end
195
+ end
data/lib/mana/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Mana
4
- VERSION = "0.5.1"
4
+ VERSION = "0.5.7"
5
5
  end
data/lib/mana.rb CHANGED
@@ -2,21 +2,13 @@
2
2
 
3
3
  require_relative "mana/version"
4
4
  require_relative "mana/config"
5
+ require_relative "mana/security_policy"
5
6
  require_relative "mana/backends/base"
6
7
  require_relative "mana/backends/anthropic"
7
8
  require_relative "mana/backends/openai"
8
- require_relative "mana/backends/registry"
9
- require_relative "mana/effect_registry"
10
- require_relative "mana/namespace"
11
9
  require_relative "mana/memory_store"
12
- require_relative "mana/context_window"
13
10
  require_relative "mana/memory"
14
- require_relative "mana/object_registry"
15
- require_relative "mana/remote_ref"
16
- require_relative "mana/engines/base"
17
- require_relative "mana/engines/llm"
18
- require_relative "mana/engines/ruby_eval"
19
- require_relative "mana/engines/detect"
11
+ require_relative "mana/logger"
20
12
  require_relative "mana/engine"
21
13
  require_relative "mana/introspect"
22
14
  require_relative "mana/compiler"
@@ -25,48 +17,33 @@ require_relative "mana/mixin"
25
17
 
26
18
  module Mana
27
19
  class Error < StandardError; end
20
+ class ConfigError < Error; end
28
21
  class MaxIterationsError < Error; end
29
22
  class LLMError < Error; end
30
23
  class MockError < Error; end
31
24
 
32
25
  class << self
26
+ # Return the global config singleton (lazy-initialized)
33
27
  def config
34
28
  @config ||= Config.new
35
29
  end
36
30
 
31
+ # Yield the config for modification, return the config instance
37
32
  def configure
38
33
  yield(config) if block_given?
39
34
  config
40
35
  end
41
36
 
37
+ # Shortcut to set the model name directly
42
38
  def model=(model)
43
39
  config.model = model
44
40
  end
45
41
 
46
- def handle(handler = nil, **opts, &block)
47
- Engine.with_handler(handler, **opts, &block)
48
- end
49
-
42
+ # Reset all global state: config, thread-local memory and mock
50
43
  def reset!
51
44
  @config = Config.new
52
- EffectRegistry.clear!
53
- Engines.reset_detector!
54
- ObjectRegistry.reset!
55
- Engines::JavaScript.reset! if defined?(Engines::JavaScript)
56
- Engines::Python.reset! if defined?(Engines::Python)
57
45
  Thread.current[:mana_memory] = nil
58
46
  Thread.current[:mana_mock] = nil
59
- Thread.current[:mana_last_engine] = nil
60
- end
61
-
62
- # Define a custom effect that becomes an LLM tool
63
- def define_effect(name, description: nil, &handler)
64
- EffectRegistry.define(name, description: description, &handler)
65
- end
66
-
67
- # Remove a custom effect
68
- def undefine_effect(name)
69
- EffectRegistry.undefine(name)
70
47
  end
71
48
 
72
49
  # Access current thread's memory
metadata CHANGED
@@ -1,43 +1,29 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ruby-mana
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.1
4
+ version: 0.5.7
5
5
  platform: ruby
6
6
  authors:
7
- - Carl
7
+ - Carl Li
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-02-24 00:00:00.000000000 Z
11
+ date: 2026-03-26 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: binding_of_caller
15
15
  requirement: !ruby/object:Gem::Requirement
16
16
  requirements:
17
- - - "~>"
17
+ - - ">="
18
18
  - !ruby/object:Gem::Version
19
19
  version: '1.0'
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
- - - "~>"
24
+ - - ">="
25
25
  - !ruby/object:Gem::Version
26
26
  version: '1.0'
27
- - !ruby/object:Gem::Dependency
28
- name: mini_racer
29
- requirement: !ruby/object:Gem::Requirement
30
- requirements:
31
- - - "~>"
32
- - !ruby/object:Gem::Version
33
- version: '0.16'
34
- type: :runtime
35
- prerelease: false
36
- version_requirements: !ruby/object:Gem::Requirement
37
- requirements:
38
- - - "~>"
39
- - !ruby/object:Gem::Version
40
- version: '0.16'
41
27
  description: |
42
28
  Mana lets you write natural language strings in Ruby that execute via LLM
43
29
  with full access to your program's live state. Read/write variables, call
@@ -50,41 +36,29 @@ files:
50
36
  - CHANGELOG.md
51
37
  - LICENSE
52
38
  - README.md
53
- - data/lang-rules.yml
54
39
  - lib/mana.rb
55
40
  - lib/mana/backends/anthropic.rb
56
41
  - lib/mana/backends/base.rb
57
42
  - lib/mana/backends/openai.rb
58
- - lib/mana/backends/registry.rb
59
43
  - lib/mana/compiler.rb
60
44
  - lib/mana/config.rb
61
- - lib/mana/context_window.rb
62
- - lib/mana/effect_registry.rb
63
45
  - lib/mana/engine.rb
64
- - lib/mana/engines/base.rb
65
- - lib/mana/engines/detect.rb
66
- - lib/mana/engines/javascript.rb
67
- - lib/mana/engines/llm.rb
68
- - lib/mana/engines/python.rb
69
- - lib/mana/engines/ruby_eval.rb
70
46
  - lib/mana/introspect.rb
47
+ - lib/mana/logger.rb
71
48
  - lib/mana/memory.rb
72
49
  - lib/mana/memory_store.rb
73
50
  - lib/mana/mixin.rb
74
51
  - lib/mana/mock.rb
75
- - lib/mana/namespace.rb
76
- - lib/mana/object_registry.rb
77
- - lib/mana/remote_ref.rb
52
+ - lib/mana/security_policy.rb
78
53
  - lib/mana/string_ext.rb
79
- - lib/mana/test.rb
80
54
  - lib/mana/version.rb
81
- homepage: https://github.com/carlnoah6/ruby-mana
55
+ homepage: https://github.com/twokidsCarl/ruby-mana
82
56
  licenses:
83
57
  - MIT
84
58
  metadata:
85
- homepage_uri: https://github.com/carlnoah6/ruby-mana
86
- source_code_uri: https://github.com/carlnoah6/ruby-mana
87
- changelog_uri: https://github.com/carlnoah6/ruby-mana/blob/main/CHANGELOG.md
59
+ homepage_uri: https://github.com/twokidsCarl/ruby-mana
60
+ source_code_uri: https://github.com/twokidsCarl/ruby-mana
61
+ changelog_uri: https://github.com/twokidsCarl/ruby-mana/blob/main/CHANGELOG.md
88
62
  post_install_message:
89
63
  rdoc_options: []
90
64
  require_paths:
@@ -93,7 +67,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
93
67
  requirements:
94
68
  - - ">="
95
69
  - !ruby/object:Gem::Version
96
- version: 3.3.0
70
+ version: '3.3'
97
71
  required_rubygems_version: !ruby/object:Gem::Requirement
98
72
  requirements:
99
73
  - - ">="
data/data/lang-rules.yml DELETED
@@ -1,196 +0,0 @@
1
- version: 1
2
- languages:
3
- javascript:
4
- strong:
5
- - "const "
6
- - "let "
7
- - "=> "
8
- - "=== "
9
- - "!== "
10
- - "console.log"
11
- - "console.error"
12
- - ".filter("
13
- - ".map("
14
- - ".reduce("
15
- - ".forEach("
16
- - ".indexOf("
17
- - ".push("
18
- - "function "
19
- - "async "
20
- - "await "
21
- - "require("
22
- - "module.exports"
23
- - "export "
24
- - "import "
25
- - "new Promise"
26
- - "document."
27
- - "window."
28
- - "JSON.parse"
29
- - "JSON.stringify"
30
- - "typeof "
31
- - "undefined"
32
- - "null;"
33
- weak:
34
- - "var "
35
- - "return "
36
- - "true"
37
- - "false"
38
- - "null"
39
- - "this."
40
- anti:
41
- - "let me"
42
- - "let us"
43
- - "let's"
44
- - "const ant"
45
- - "import ant"
46
- - "import this"
47
- patterns:
48
- - "\\bfunction\\s+\\w+\\s*\\("
49
- - "\\(\\s*\\w+\\s*\\)\\s*=>"
50
- - "\\bclass\\s+\\w+\\s*\\{"
51
-
52
- python:
53
- strong:
54
- - "elif "
55
- - "print("
56
- - "__init__"
57
- - "__name__"
58
- - "self."
59
- - "import numpy"
60
- - "import pandas"
61
- - "lambda "
62
- - "except "
63
- - "finally:"
64
- - "nonlocal "
65
- weak:
66
- - "def "
67
- - "from "
68
- - "yield "
69
- - "raise "
70
- - "assert "
71
- - "global "
72
- - "import "
73
- - "return "
74
- - "class "
75
- - "for "
76
- - "while "
77
- - "if "
78
- - "is not"
79
- - "not in"
80
- - "pass "
81
- anti:
82
- - "import this"
83
- - "import that"
84
- - "from here"
85
- - "from there"
86
- - "for example"
87
- - "for instance"
88
- - "while I"
89
- - "while we"
90
- - "if you"
91
- - "if we"
92
- - "with you"
93
- - "with the"
94
- - "with a "
95
- - "with my"
96
- - "and the"
97
- - "and I"
98
- - "and we"
99
- - "or the"
100
- - "or a "
101
- - "or I"
102
- - "or False"
103
- - "True or"
104
- - "as a "
105
- - "as the"
106
- - "as I"
107
- - "pass the"
108
- - "pass it"
109
- - "pass on"
110
- patterns:
111
- - "\\[.+\\bfor\\b.+\\bin\\b.+\\]"
112
- - "\\bdef\\s+\\w+\\s*\\("
113
- - "\\bclass\\s+\\w+\\s*[:(]"
114
- - "^\\s{4}"
115
-
116
- ruby:
117
- strong:
118
- - "puts "
119
- - "require "
120
- - "require_relative"
121
- - "attr_accessor"
122
- - "attr_reader"
123
- - "attr_writer"
124
- - ".each "
125
- - ".map "
126
- - ".select "
127
- - ".reject "
128
- - "do |"
129
- - "def "
130
- - "module "
131
- - "rescue "
132
- - "ensure "
133
- - "unless "
134
- - "until "
135
- - "elsif "
136
- - "nil"
137
- - "p "
138
- - "pp "
139
- - "Proc.new"
140
- - "lambda {"
141
- - "-> {"
142
- weak:
143
- - "end "
144
- - "begin "
145
- - "class "
146
- - "return "
147
- - "if "
148
- - "for "
149
- - "while "
150
- anti:
151
- - "the end"
152
- - "in the end"
153
- - "at the end"
154
- - "begin with"
155
- - "begin to"
156
- patterns:
157
- - "\\bdo\\s*\\|\\w+\\|"
158
- - "\\bdef\\s+\\w+[?!]?"
159
- - "\\bend\\b"
160
-
161
- natural_language:
162
- strong:
163
- - "please "
164
- - "analyze "
165
- - "summarize "
166
- - "translate "
167
- - "explain "
168
- - "find "
169
- - "calculate "
170
- - "generate "
171
- - "create "
172
- - "write "
173
- - "describe "
174
- - "compare "
175
- - "store in <"
176
- - "save in <"
177
- - "put in <"
178
- - ", store "
179
- - ", save "
180
- weak:
181
- - "the "
182
- - "a "
183
- - "an "
184
- - "is "
185
- - "are "
186
- - "was "
187
- - "were "
188
- - "what "
189
- - "how "
190
- - "why "
191
- - "when "
192
- - "where "
193
- - "which "
194
- anti: []
195
- patterns:
196
- - "<\\w+>"