ruby-mana 0.5.8 → 0.5.10
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 +19 -0
- data/README.md +114 -195
- data/exe/mana +12 -0
- data/lib/mana/backends/anthropic.rb +47 -0
- data/lib/mana/backends/base.rb +41 -0
- data/lib/mana/backends/openai.rb +15 -0
- data/lib/mana/binding_helpers.rb +106 -0
- data/lib/mana/chat.rb +301 -0
- data/lib/mana/config.rb +0 -19
- data/lib/mana/engine.rb +102 -359
- data/lib/mana/knowledge.rb +203 -0
- data/lib/mana/logger.rb +10 -0
- data/lib/mana/prompt_builder.rb +157 -0
- data/lib/mana/tool_handler.rb +180 -0
- data/lib/mana/version.rb +1 -1
- data/lib/mana.rb +10 -1
- metadata +38 -4
- data/lib/mana/security_policy.rb +0 -195
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Mana
|
|
4
|
+
# Binding manipulation utilities: read/write variables, validate names, serialize values.
|
|
5
|
+
# Mixed into Engine as private methods.
|
|
6
|
+
module BindingHelpers
|
|
7
|
+
VALID_IDENTIFIER = /\A[A-Za-z_][A-Za-z0-9_]*\z/
|
|
8
|
+
|
|
9
|
+
private
|
|
10
|
+
|
|
11
|
+
# Ensure a name is a valid Ruby identifier (prevents injection)
|
|
12
|
+
def validate_name!(name)
|
|
13
|
+
raise Mana::Error, "invalid identifier: #{name.inspect}" unless name.match?(VALID_IDENTIFIER)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# Resolve a name to a value: try local variable first, then receiver method
|
|
17
|
+
def resolve(name)
|
|
18
|
+
validate_name!(name)
|
|
19
|
+
if @binding.local_variable_defined?(name.to_sym)
|
|
20
|
+
# Found as a local variable in the caller's binding
|
|
21
|
+
@binding.local_variable_get(name.to_sym)
|
|
22
|
+
elsif @binding.receiver.respond_to?(name.to_sym)
|
|
23
|
+
# Found as a public method on the caller's self
|
|
24
|
+
@binding.receiver.public_send(name.to_sym)
|
|
25
|
+
else
|
|
26
|
+
raise NameError, "undefined variable or method '#{name}'"
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Write a value into the caller's binding, with Ruby 4.0+ singleton method fallback.
|
|
31
|
+
# Only defines a singleton method when the variable doesn't already exist as a local
|
|
32
|
+
# AND the receiver doesn't already have a real method with that name.
|
|
33
|
+
def write_local(name, value)
|
|
34
|
+
validate_name!(name)
|
|
35
|
+
sym = name.to_sym
|
|
36
|
+
|
|
37
|
+
# Check if the variable already exists before setting
|
|
38
|
+
existed = @binding.local_variable_defined?(sym)
|
|
39
|
+
@binding.local_variable_set(sym, value)
|
|
40
|
+
|
|
41
|
+
# Ruby 4.0+: local_variable_set can no longer create new locals visible
|
|
42
|
+
# in the caller's scope. Define a singleton method ONLY for new variables
|
|
43
|
+
# that don't conflict with existing methods on the receiver.
|
|
44
|
+
unless existed
|
|
45
|
+
receiver = @binding.eval("self")
|
|
46
|
+
# Don't overwrite real instance methods — only add if no method exists
|
|
47
|
+
unless receiver.class.method_defined?(sym) || receiver.class.private_method_defined?(sym)
|
|
48
|
+
old_verbose, $VERBOSE = $VERBOSE, nil
|
|
49
|
+
receiver.define_singleton_method(sym) { value }
|
|
50
|
+
$VERBOSE = old_verbose
|
|
51
|
+
# Track Mana-created singleton method variables so local_variables can include them
|
|
52
|
+
mana_vars = receiver.instance_variable_defined?(:@__mana_vars__) ? receiver.instance_variable_get(:@__mana_vars__) : Set.new
|
|
53
|
+
mana_vars << sym
|
|
54
|
+
receiver.instance_variable_set(:@__mana_vars__, mana_vars)
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Find the user's source file by walking up the call stack.
|
|
60
|
+
# Used for introspecting available methods in the caller's code.
|
|
61
|
+
def caller_source_path
|
|
62
|
+
# Try binding's source_location first (most direct)
|
|
63
|
+
loc = @binding.source_location
|
|
64
|
+
return loc[0] if loc.is_a?(Array)
|
|
65
|
+
|
|
66
|
+
# Fallback: scan caller_locations, skip frames inside the mana gem itself
|
|
67
|
+
caller_locations(4, 20)&.each do |frame|
|
|
68
|
+
path = frame.absolute_path || frame.path
|
|
69
|
+
next if path.nil? || path.include?("mana/")
|
|
70
|
+
return path
|
|
71
|
+
end
|
|
72
|
+
nil
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Serialize a Ruby value to a string representation the LLM can understand.
|
|
76
|
+
# Handles primitives, collections, and arbitrary objects (via ivar inspection).
|
|
77
|
+
def serialize_value(val)
|
|
78
|
+
case val
|
|
79
|
+
when Time
|
|
80
|
+
# Format Time as a human-readable timestamp string
|
|
81
|
+
val.strftime("%Y-%m-%d %H:%M:%S %z").inspect
|
|
82
|
+
when String, Integer, Float, TrueClass, FalseClass, NilClass
|
|
83
|
+
# Primitives: use Ruby's built-in inspect
|
|
84
|
+
val.inspect
|
|
85
|
+
when Symbol
|
|
86
|
+
# Convert symbol to string for LLM readability
|
|
87
|
+
val.to_s.inspect
|
|
88
|
+
when Array
|
|
89
|
+
# Recursively serialize each element
|
|
90
|
+
"[#{val.map { |v| serialize_value(v) }.join(', ')}]"
|
|
91
|
+
when Hash
|
|
92
|
+
# Recursively serialize key-value pairs
|
|
93
|
+
pairs = val.map { |k, v| "#{serialize_value(k)} => #{serialize_value(v)}" }
|
|
94
|
+
"{#{pairs.join(', ')}}"
|
|
95
|
+
else
|
|
96
|
+
# Arbitrary object: show class name and instance variables
|
|
97
|
+
ivars = val.instance_variables
|
|
98
|
+
obj_repr = ivars.map do |ivar|
|
|
99
|
+
attr_name = ivar.to_s.delete_prefix("@")
|
|
100
|
+
"#{attr_name}: #{val.instance_variable_get(ivar).inspect}" rescue nil
|
|
101
|
+
end.compact.join(", ")
|
|
102
|
+
"#<#{val.class} #{obj_repr}>"
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
data/lib/mana/chat.rb
ADDED
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Mana
|
|
4
|
+
# Interactive chat mode — enter with Mana.chat to talk to Mana in your Ruby runtime.
|
|
5
|
+
# Supports streaming output, colored prompts, and full access to the caller's binding.
|
|
6
|
+
# Auto-detects Ruby code vs natural language. Use '!' prefix to force Ruby execution.
|
|
7
|
+
module Chat
|
|
8
|
+
USER_PROMPT = "\e[36mmana>\e[0m " # cyan
|
|
9
|
+
MANA_PREFIX = "\e[33mmana>\e[0m " # yellow
|
|
10
|
+
RUBY_PREFIX = "\e[35m=>\e[0m " # magenta
|
|
11
|
+
THINK_COLOR = "\e[3;36m" # italic cyan
|
|
12
|
+
TOOL_COLOR = "\e[2;33m" # dim yellow
|
|
13
|
+
RESULT_COLOR = "\e[2;32m" # dim green
|
|
14
|
+
CODE_COLOR = "\e[36m" # cyan for code
|
|
15
|
+
BOLD = "\e[1m" # bold
|
|
16
|
+
ERROR_COLOR = "\e[31m" # red
|
|
17
|
+
DIM = "\e[2m" # dim
|
|
18
|
+
RESET = "\e[0m"
|
|
19
|
+
|
|
20
|
+
CONT_PROMPT = "\e[2m \e[0m"
|
|
21
|
+
EXIT_COMMANDS = /\A(exit|quit|bye|q)\z/i
|
|
22
|
+
|
|
23
|
+
# Entry point — starts the REPL loop with Reline for readline support.
|
|
24
|
+
# Reline is required lazily so chat mode doesn't penalize non-interactive usage.
|
|
25
|
+
HISTORY_FILE = File.join(Dir.home, ".mana_history")
|
|
26
|
+
HISTORY_MAX = 1000
|
|
27
|
+
|
|
28
|
+
def self.start(caller_binding)
|
|
29
|
+
require "reline"
|
|
30
|
+
load_history
|
|
31
|
+
puts "#{DIM}Mana chat · type 'exit' to quit#{RESET}"
|
|
32
|
+
puts
|
|
33
|
+
|
|
34
|
+
loop do
|
|
35
|
+
input = read_input
|
|
36
|
+
break if input.nil?
|
|
37
|
+
next if input.strip.empty?
|
|
38
|
+
break if input.strip.match?(EXIT_COMMANDS)
|
|
39
|
+
|
|
40
|
+
# Three-tier dispatch:
|
|
41
|
+
# "!" prefix — force Ruby eval (bypass ambiguity detection)
|
|
42
|
+
# Valid Ruby syntax — try Ruby first, fall back to LLM if NameError
|
|
43
|
+
# Everything else — send directly to the LLM
|
|
44
|
+
if input.start_with?("!")
|
|
45
|
+
eval_ruby(caller_binding, input[1..].strip)
|
|
46
|
+
elsif ruby_syntax?(input)
|
|
47
|
+
eval_ruby(caller_binding, input) { run_mana(caller_binding, input) }
|
|
48
|
+
else
|
|
49
|
+
run_mana(caller_binding, input)
|
|
50
|
+
end
|
|
51
|
+
puts
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
save_history
|
|
55
|
+
puts "#{DIM}bye!#{RESET}"
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def self.load_history
|
|
59
|
+
return unless File.exist?(HISTORY_FILE)
|
|
60
|
+
|
|
61
|
+
File.readlines(HISTORY_FILE, chomp: true).last(HISTORY_MAX).each do |line|
|
|
62
|
+
Reline::HISTORY << line
|
|
63
|
+
end
|
|
64
|
+
rescue StandardError
|
|
65
|
+
# ignore corrupt history
|
|
66
|
+
end
|
|
67
|
+
private_class_method :load_history
|
|
68
|
+
|
|
69
|
+
def self.save_history
|
|
70
|
+
lines = Reline::HISTORY.to_a.last(HISTORY_MAX)
|
|
71
|
+
File.write(HISTORY_FILE, lines.join("\n") + "\n")
|
|
72
|
+
rescue StandardError
|
|
73
|
+
# ignore write failures
|
|
74
|
+
end
|
|
75
|
+
private_class_method :save_history
|
|
76
|
+
|
|
77
|
+
# Reads input with multi-line support — keeps prompting with continuation
|
|
78
|
+
# markers while the buffer contains incomplete Ruby (unclosed blocks, strings, etc.)
|
|
79
|
+
def self.read_input
|
|
80
|
+
# Second arg to readline: true = add to history, false = don't
|
|
81
|
+
buffer = Reline.readline(USER_PROMPT, true)
|
|
82
|
+
return nil if buffer.nil?
|
|
83
|
+
|
|
84
|
+
while incomplete_ruby?(buffer)
|
|
85
|
+
line = Reline.readline(CONT_PROMPT, false)
|
|
86
|
+
break if line.nil?
|
|
87
|
+
buffer += "\n" + line
|
|
88
|
+
end
|
|
89
|
+
buffer
|
|
90
|
+
end
|
|
91
|
+
private_class_method :read_input
|
|
92
|
+
|
|
93
|
+
# Heuristic: if RubyVM can compile it, treat as Ruby code.
|
|
94
|
+
# This lets users type `x = 1 + 2` without needing the "!" prefix.
|
|
95
|
+
def self.ruby_syntax?(input)
|
|
96
|
+
RubyVM::InstructionSequence.compile(input)
|
|
97
|
+
true
|
|
98
|
+
rescue SyntaxError
|
|
99
|
+
false
|
|
100
|
+
end
|
|
101
|
+
private_class_method :ruby_syntax?
|
|
102
|
+
|
|
103
|
+
# Distinguishes incomplete code (needs more input) from invalid code (syntax error).
|
|
104
|
+
# Only "unexpected end-of-input" and "unterminated" indicate the user is still typing;
|
|
105
|
+
# other SyntaxErrors mean the code is complete but malformed.
|
|
106
|
+
def self.incomplete_ruby?(code)
|
|
107
|
+
RubyVM::InstructionSequence.compile(code)
|
|
108
|
+
false
|
|
109
|
+
rescue SyntaxError => e
|
|
110
|
+
e.message.include?("unexpected end-of-input") ||
|
|
111
|
+
e.message.include?("unterminated")
|
|
112
|
+
end
|
|
113
|
+
private_class_method :incomplete_ruby?
|
|
114
|
+
|
|
115
|
+
# Eval Ruby in the caller's binding. On NameError/NoMethodError, yields to the
|
|
116
|
+
# fallback block (which sends to the LLM) — this is how ambiguous input like
|
|
117
|
+
# "sort this list" gets routed: Ruby rejects it, then the LLM handles it.
|
|
118
|
+
def self.eval_ruby(caller_binding, code)
|
|
119
|
+
result = caller_binding.eval(code)
|
|
120
|
+
puts "#{RUBY_PREFIX}#{result.inspect}"
|
|
121
|
+
rescue NameError, NoMethodError => e
|
|
122
|
+
block_given? ? yield : puts("#{ERROR_COLOR}#{e.class}: #{e.message}#{RESET}")
|
|
123
|
+
rescue => e
|
|
124
|
+
puts "#{ERROR_COLOR}#{e.class}: #{e.message}#{RESET}"
|
|
125
|
+
end
|
|
126
|
+
private_class_method :eval_ruby
|
|
127
|
+
|
|
128
|
+
# --- Mana LLM execution with streaming + markdown rendering ---
|
|
129
|
+
|
|
130
|
+
# Executes a prompt via the LLM engine with streaming output.
|
|
131
|
+
# Uses a line buffer to render complete lines with markdown formatting
|
|
132
|
+
# while the LLM streams tokens incrementally.
|
|
133
|
+
def self.run_mana(caller_binding, input)
|
|
134
|
+
streaming_text = false
|
|
135
|
+
in_code_block = false
|
|
136
|
+
line_buffer = +"" # mutable string — accumulates partial lines until \n
|
|
137
|
+
engine = Engine.new(caller_binding)
|
|
138
|
+
|
|
139
|
+
begin
|
|
140
|
+
result = engine.execute(input) do |type, *args|
|
|
141
|
+
case type
|
|
142
|
+
when :text
|
|
143
|
+
unless streaming_text
|
|
144
|
+
print MANA_PREFIX
|
|
145
|
+
streaming_text = true
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# Buffer text and flush complete lines with markdown rendering
|
|
149
|
+
line_buffer << args[0].to_s
|
|
150
|
+
while (idx = line_buffer.index("\n"))
|
|
151
|
+
line = line_buffer.slice!(0, idx + 1)
|
|
152
|
+
in_code_block = render_line(line.chomp, in_code_block)
|
|
153
|
+
puts
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
when :tool_start
|
|
157
|
+
flush_line_buffer(line_buffer, in_code_block) if streaming_text
|
|
158
|
+
streaming_text = false
|
|
159
|
+
in_code_block = false
|
|
160
|
+
line_buffer.clear
|
|
161
|
+
name, input_data = args
|
|
162
|
+
detail = format_tool_call(name, input_data)
|
|
163
|
+
puts "#{TOOL_COLOR} ⚡ #{detail}#{RESET}"
|
|
164
|
+
|
|
165
|
+
when :tool_end
|
|
166
|
+
name, result_str = args
|
|
167
|
+
summary = truncate(result_str.to_s, 120)
|
|
168
|
+
puts "#{RESULT_COLOR} ↩ #{summary}#{RESET}" unless summary.start_with?("ok:")
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
# Flush any remaining buffered text
|
|
173
|
+
flush_line_buffer(line_buffer, in_code_block) if streaming_text
|
|
174
|
+
|
|
175
|
+
# Non-streaming fallback: if no text was streamed (e.g. tool-only response),
|
|
176
|
+
# render the final result as a single block
|
|
177
|
+
unless streaming_text
|
|
178
|
+
display = case result
|
|
179
|
+
when Hash then result.inspect
|
|
180
|
+
when nil then nil
|
|
181
|
+
when String then render_markdown(result)
|
|
182
|
+
else result.inspect
|
|
183
|
+
end
|
|
184
|
+
puts "#{MANA_PREFIX}#{display}" if display
|
|
185
|
+
end
|
|
186
|
+
rescue LLMError, MaxIterationsError => e
|
|
187
|
+
flush_line_buffer(line_buffer, in_code_block) if streaming_text
|
|
188
|
+
puts "#{ERROR_COLOR}error: #{e.message}#{RESET}"
|
|
189
|
+
end
|
|
190
|
+
end
|
|
191
|
+
private_class_method :run_mana
|
|
192
|
+
|
|
193
|
+
# --- Markdown → ANSI rendering ---
|
|
194
|
+
|
|
195
|
+
# Render a single line, handling code block state.
|
|
196
|
+
# Returns the new in_code_block state.
|
|
197
|
+
def self.render_line(line, in_code_block)
|
|
198
|
+
if line.strip.start_with?("```")
|
|
199
|
+
if in_code_block
|
|
200
|
+
# End of code block — don't print the closing ```
|
|
201
|
+
return false
|
|
202
|
+
else
|
|
203
|
+
# Start of code block — don't print the opening ```
|
|
204
|
+
return true
|
|
205
|
+
end
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
if in_code_block
|
|
209
|
+
print " #{CODE_COLOR}#{line}#{RESET}"
|
|
210
|
+
else
|
|
211
|
+
print render_markdown_inline(line)
|
|
212
|
+
end
|
|
213
|
+
in_code_block
|
|
214
|
+
end
|
|
215
|
+
private_class_method :render_line
|
|
216
|
+
|
|
217
|
+
# Flush remaining text in the line buffer
|
|
218
|
+
def self.flush_line_buffer(buffer, in_code_block)
|
|
219
|
+
return if buffer.empty?
|
|
220
|
+
text = buffer.dup
|
|
221
|
+
buffer.clear
|
|
222
|
+
if in_code_block
|
|
223
|
+
print " #{CODE_COLOR}#{text}#{RESET}"
|
|
224
|
+
else
|
|
225
|
+
print render_markdown_inline(text)
|
|
226
|
+
end
|
|
227
|
+
puts
|
|
228
|
+
end
|
|
229
|
+
private_class_method :flush_line_buffer
|
|
230
|
+
|
|
231
|
+
# Convert inline markdown to ANSI codes.
|
|
232
|
+
# Handles **bold**, `inline code` (with negative lookbehind to skip ```),
|
|
233
|
+
# and # headings. Intentionally minimal — just enough for readable terminal output.
|
|
234
|
+
def self.render_markdown_inline(text)
|
|
235
|
+
text
|
|
236
|
+
.gsub(/\*\*(.+?)\*\*/, "#{BOLD}\\1#{RESET}")
|
|
237
|
+
.gsub(/(?<!`)`([^`]+)`(?!`)/, "#{CODE_COLOR}\\1#{RESET}")
|
|
238
|
+
.gsub(/^\#{1,3}\s+(.+)/) { BOLD + $1 + RESET }
|
|
239
|
+
end
|
|
240
|
+
private_class_method :render_markdown_inline
|
|
241
|
+
|
|
242
|
+
# Render a complete block of markdown text (for non-streaming results)
|
|
243
|
+
def self.render_markdown(text)
|
|
244
|
+
lines = text.lines
|
|
245
|
+
result = +""
|
|
246
|
+
in_code = false
|
|
247
|
+
lines.each do |line|
|
|
248
|
+
stripped = line.strip
|
|
249
|
+
if stripped.start_with?("```")
|
|
250
|
+
in_code = !in_code
|
|
251
|
+
next
|
|
252
|
+
end
|
|
253
|
+
if in_code
|
|
254
|
+
result << " #{CODE_COLOR}#{line.rstrip}#{RESET}\n"
|
|
255
|
+
else
|
|
256
|
+
result << render_markdown_inline(line.rstrip) << "\n"
|
|
257
|
+
end
|
|
258
|
+
end
|
|
259
|
+
result.chomp
|
|
260
|
+
end
|
|
261
|
+
private_class_method :render_markdown
|
|
262
|
+
|
|
263
|
+
# --- Tool formatting helpers ---
|
|
264
|
+
|
|
265
|
+
def self.format_tool_call(name, input)
|
|
266
|
+
case name
|
|
267
|
+
when "call_func"
|
|
268
|
+
func = input[:name] || input["name"]
|
|
269
|
+
args = input[:args] || input["args"] || []
|
|
270
|
+
body = input[:body] || input["body"]
|
|
271
|
+
desc = func.to_s
|
|
272
|
+
desc += "(#{args.map(&:inspect).join(', ')})" if args.any?
|
|
273
|
+
desc += " { #{truncate(body, 40)} }" if body
|
|
274
|
+
desc
|
|
275
|
+
# Display read_var as just the name, write_var as an assignment
|
|
276
|
+
when "read_var", "write_var"
|
|
277
|
+
var = input[:name] || input["name"]
|
|
278
|
+
val = input[:value] || input["value"]
|
|
279
|
+
val ? "#{var} = #{truncate(val.inspect, 60)}" : var.to_s
|
|
280
|
+
when "read_attr", "write_attr"
|
|
281
|
+
obj = input[:obj] || input["obj"]
|
|
282
|
+
attr = input[:attr] || input["attr"]
|
|
283
|
+
"#{obj}.#{attr}"
|
|
284
|
+
when "remember"
|
|
285
|
+
content = input[:content] || input["content"]
|
|
286
|
+
"remember: #{truncate(content.to_s, 60)}"
|
|
287
|
+
when "knowledge"
|
|
288
|
+
topic = input[:topic] || input["topic"]
|
|
289
|
+
"knowledge(#{topic})"
|
|
290
|
+
else
|
|
291
|
+
name.to_s
|
|
292
|
+
end
|
|
293
|
+
end
|
|
294
|
+
private_class_method :format_tool_call
|
|
295
|
+
|
|
296
|
+
def self.truncate(str, max)
|
|
297
|
+
str.length > max ? "#{str[0, max]}..." : str
|
|
298
|
+
end
|
|
299
|
+
private_class_method :truncate
|
|
300
|
+
end
|
|
301
|
+
end
|
data/lib/mana/config.rb
CHANGED
|
@@ -34,8 +34,6 @@ module Mana
|
|
|
34
34
|
self.timeout = (ENV["MANA_TIMEOUT"] || 120).to_i
|
|
35
35
|
@verbose = %w[1 true yes].include?(ENV["MANA_VERBOSE"]&.downcase)
|
|
36
36
|
@backend = ENV["MANA_BACKEND"]&.to_sym
|
|
37
|
-
sec = ENV["MANA_SECURITY"]
|
|
38
|
-
@security = SecurityPolicy.new(sec ? sec.to_sym : :standard)
|
|
39
37
|
@namespace = nil
|
|
40
38
|
@memory_store = nil
|
|
41
39
|
@memory_path = nil
|
|
@@ -55,23 +53,6 @@ module Mana
|
|
|
55
53
|
@timeout = value
|
|
56
54
|
end
|
|
57
55
|
|
|
58
|
-
# Read the current security policy
|
|
59
|
-
def security
|
|
60
|
-
@security
|
|
61
|
-
end
|
|
62
|
-
|
|
63
|
-
# Accept Symbol (:strict), Integer (1), or SecurityPolicy instance
|
|
64
|
-
def security=(value)
|
|
65
|
-
case value
|
|
66
|
-
when SecurityPolicy
|
|
67
|
-
@security = value
|
|
68
|
-
when Symbol, Integer
|
|
69
|
-
@security = SecurityPolicy.new(value)
|
|
70
|
-
else
|
|
71
|
-
raise ArgumentError, "security must be a Symbol, Integer, or SecurityPolicy, got #{value.class}"
|
|
72
|
-
end
|
|
73
|
-
end
|
|
74
|
-
|
|
75
56
|
# Resolve the effective base URL based on the configured or auto-detected backend.
|
|
76
57
|
# Falls back to the appropriate default when no explicit URL is set.
|
|
77
58
|
def effective_base_url
|