ruby-utcp 1.1.2 → 1.1.3

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.
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ module UTCP
4
+ class CodeMode
5
+ class Evaluator
6
+ private
7
+
8
+ def invoke_runtime_api(method_name, arguments, block)
9
+ raise CodeModeSyntaxError, "codemode.#{method_name} does not accept a block" if block
10
+
11
+ case method_name
12
+ when "call_tool"
13
+ require_arity!("codemode.call_tool", arguments, 1..2)
14
+ safe_tool_value(@client.call_tool(arguments[0], tool_arguments(arguments[1])))
15
+ when "call_tool_stream", "call_tool_streaming"
16
+ require_arity!("codemode.#{method_name}", arguments, 1..2)
17
+ safe_tool_value(@client.call_tool_streaming(arguments[0], tool_arguments(arguments[1])).to_a)
18
+ when "search_tools"
19
+ require_arity!("codemode.search_tools", arguments, 1..2)
20
+ options = arguments[1].is_a?(Hash) ? arguments[1] : {}
21
+ limit = arguments[1].is_a?(Numeric) ? arguments[1] : options.fetch("limit", 10)
22
+ safe_tool_value(@client.search_tools(arguments[0].to_s, limit: Integer(limit)).map do |tool|
23
+ InterfaceGenerator.tool_descriptor(tool)
24
+ end)
25
+ when "get_tool_interface"
26
+ require_arity!("codemode.get_tool_interface", arguments, 1)
27
+ tool = @client.list_tools.find { |candidate| candidate.name == arguments.first.to_s }
28
+ raise ToolNotFoundError, "Tool not found: #{arguments.first}" unless tool
29
+ safe_tool_value(InterfaceGenerator.tool_descriptor(tool))
30
+ when "interfaces"
31
+ require_arity!("codemode.interfaces", arguments, 0)
32
+ @interfaces.dup
33
+ when "get"
34
+ runtime_get(arguments)
35
+ else
36
+ raise CodeModeSyntaxError, "Method #{method_name.inspect} is not available on codemode"
37
+ end
38
+ end
39
+
40
+ def runtime_get(arguments)
41
+ require_arity!("codemode.get", arguments, 2..3)
42
+ receiver, key, default = arguments
43
+ return receiver.fetch(key, default) if receiver.is_a?(Hash)
44
+ return receiver.fetch(Integer(key), default) if receiver.is_a?(Array)
45
+ return receiver[key] || default if receiver.is_a?(String)
46
+
47
+ raise CodeModeExecutionError, "codemode.get requires a hash, array, or string"
48
+ rescue IndexError, TypeError
49
+ default
50
+ end
51
+
52
+ def tool_arguments(value)
53
+ return {} if value.nil?
54
+ raise CodeModeSyntaxError, "Tool arguments must be a hash" unless value.is_a?(Hash)
55
+
56
+ safe_tool_value(value)
57
+ end
58
+
59
+ def log(kind, values)
60
+ rendered = values.map { |value| kind == "p" ? value.inspect : value.to_s }.join(kind == "print" ? "" : " ")
61
+ rendered = "[WARN] #{rendered}" if kind == "warn"
62
+ bytes = rendered.bytesize
63
+ raise CodeModeLimitError, "Code Mode log limit exceeded" if @log_bytes + bytes > MAX_LOG_BYTES
64
+
65
+ @logs << rendered
66
+ @log_bytes += bytes
67
+ end
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,93 @@
1
+ # frozen_string_literal: true
2
+
3
+ module UTCP
4
+ class CodeMode
5
+ class Evaluator
6
+ private
7
+
8
+ def preflight_binary_size!(left, operator, right)
9
+ if operator == :** && right.is_a?(Integer)
10
+ bases = left.is_a?(Rational) ? [left.numerator, left.denominator] : [left]
11
+ bases.each { |base| preflight_integer_power!(base, right) if base.is_a?(Integer) }
12
+ elsif operator == :* && left.is_a?(Integer) && right.is_a?(Integer)
13
+ if !left.zero? && !right.zero? && left.abs.bit_length + right.abs.bit_length > MAX_INTEGER_BITS
14
+ raise CodeModeLimitError, "Integer product exceeds #{MAX_VALUE_BYTES} bytes"
15
+ end
16
+ elsif operator == :+ && left.is_a?(String) && right.is_a?(String)
17
+ raise CodeModeLimitError, "String result exceeds #{MAX_VALUE_BYTES} bytes" if left.bytesize + right.bytesize > MAX_VALUE_BYTES
18
+ elsif operator == :+ && left.is_a?(Array) && right.is_a?(Array)
19
+ raise CodeModeLimitError, "Collection result exceeds #{MAX_VALUE_ITEMS} items" if left.length + right.length > MAX_VALUE_ITEMS
20
+ elsif operator == :* && right.is_a?(Integer) && (left.is_a?(String) || left.is_a?(Array))
21
+ size = left.is_a?(String) ? left.bytesize : left.length
22
+ limit = left.is_a?(String) ? MAX_VALUE_BYTES : MAX_VALUE_ITEMS
23
+ raise CodeModeLimitError, "Repeated value exceeds Code Mode limits" if right.positive? && size > limit / right
24
+ end
25
+ end
26
+
27
+ def preflight_integer_power!(base, exponent)
28
+ magnitude = base.abs
29
+ return if magnitude <= 1 || exponent.zero?
30
+
31
+ # Use an upper bound without constructing the potentially enormous result.
32
+ bits = magnitude.bit_length
33
+ bits -= 1 if (magnitude & (magnitude - 1)).zero?
34
+ if exponent.abs > (MAX_INTEGER_BITS - 1) / bits
35
+ raise CodeModeLimitError, "Integer power exceeds #{MAX_VALUE_BYTES} bytes"
36
+ end
37
+ end
38
+
39
+ def numeric_bytes(value)
40
+ case value
41
+ when Integer then [(value.abs.bit_length + 7) / 8, 1].max
42
+ when Rational then numeric_bytes(value.numerator) + numeric_bytes(value.denominator)
43
+ when Complex then numeric_bytes(value.real) + numeric_bytes(value.imaginary)
44
+ when Float
45
+ raise CodeModeLimitError, "Numeric result must be finite" unless value.finite?
46
+ 8
47
+ else value.to_s.bytesize
48
+ end
49
+ end
50
+
51
+ def bounded_value!(value)
52
+ if value.is_a?(Numeric) && numeric_bytes(value) > MAX_VALUE_BYTES
53
+ raise CodeModeLimitError, "Numeric result exceeds #{MAX_VALUE_BYTES} bytes"
54
+ end
55
+ if value.is_a?(String) && value.bytesize > MAX_VALUE_BYTES
56
+ raise CodeModeLimitError, "String result exceeds #{MAX_VALUE_BYTES} bytes"
57
+ end
58
+ if (value.is_a?(Array) || value.is_a?(Hash)) && value.length > MAX_VALUE_ITEMS
59
+ raise CodeModeLimitError, "Collection result exceeds #{MAX_VALUE_ITEMS} items"
60
+ end
61
+ value
62
+ end
63
+
64
+ def safe_tool_value(value, depth = 0, budget = { items: MAX_VALUE_ITEMS, bytes: MAX_VALUE_BYTES })
65
+ raise CodeModeLimitError, "Tool result nesting is too deep" if depth > 64
66
+ budget[:items] -= 1
67
+ raise CodeModeLimitError, "Tool result contains too many values" if budget[:items].negative?
68
+
69
+ case value
70
+ when String
71
+ budget[:bytes] -= value.bytesize
72
+ raise CodeModeLimitError, "Tool result contains too much string data" if budget[:bytes].negative?
73
+ value.dup
74
+ when Numeric
75
+ budget[:bytes] -= numeric_bytes(value)
76
+ raise CodeModeLimitError, "Tool result contains too much numeric data" if budget[:bytes].negative?
77
+ value
78
+ when nil, true, false
79
+ value
80
+ when Array
81
+ value.map { |item| safe_tool_value(item, depth + 1, budget) }
82
+ when Hash
83
+ value.each_with_object({}) do |(key, item), result|
84
+ safe_key = key.is_a?(Symbol) ? key : key.to_s
85
+ result[safe_key] = safe_tool_value(item, depth + 1, budget)
86
+ end
87
+ else
88
+ raise CodeModeExecutionError, "Tool returned unsupported #{value.class} value"
89
+ end
90
+ end
91
+ end
92
+ end
93
+ end
@@ -0,0 +1,204 @@
1
+ # frozen_string_literal: true
2
+
3
+ module UTCP
4
+ class CodeMode
5
+ class Evaluator
6
+ private
7
+
8
+ def invoke_method(receiver, method_name, arguments, block)
9
+ return invoke_runtime_api(method_name, arguments, block) if receiver.is_a?(RuntimeAPI)
10
+ return receiver[method_name] if receiver.is_a?(Hash) && arguments.empty? && !block && receiver.key?(method_name)
11
+
12
+ unless SAFE_ENUMERABLE_METHODS.include?(method_name) || SAFE_VALUE_METHODS.include?(method_name)
13
+ raise CodeModeSyntaxError, "Method #{method_name.inspect} is not available in Code Mode"
14
+ end
15
+
16
+ bounded_value!(invoke_safe_value_method(receiver, method_name, arguments, block))
17
+ end
18
+
19
+ def invoke_safe_value_method(receiver, method_name, arguments, block)
20
+ return invoke_enumerable(receiver, method_name, arguments, block) if SAFE_ENUMERABLE_METHODS.include?(method_name)
21
+
22
+ case method_name
23
+ when "length", "size" then require_arity!(method_name, arguments, 0).then { receiver.respond_to?(:length) ? receiver.length : invalid_receiver!(receiver, method_name) }
24
+ when "empty?" then require_arity!(method_name, arguments, 0).then { receiver.respond_to?(:empty?) ? receiver.empty? : invalid_receiver!(receiver, method_name) }
25
+ when "nil?" then require_arity!(method_name, arguments, 0).then { receiver.nil? }
26
+ when "to_s" then require_arity!(method_name, arguments, 0).then { receiver.to_s }
27
+ when "inspect" then require_arity!(method_name, arguments, 0).then { receiver.inspect }
28
+ when "to_i" then require_arity!(method_name, arguments, 0).then { receiver.to_i }
29
+ when "to_f" then require_arity!(method_name, arguments, 0).then { receiver.to_f }
30
+ when "to_a" then require_arity!(method_name, arguments, 0).then { safe_to_a(receiver) }
31
+ when "to_h" then require_arity!(method_name, arguments, 0).then { receiver.is_a?(Hash) ? receiver.dup : invalid_receiver!(receiver, method_name) }
32
+ when "first" then require_arity!(method_name, arguments, 0..1).then { enumerable_value!(receiver, method_name).first(*arguments) }
33
+ when "last" then require_arity!(method_name, arguments, 0..1).then { enumerable_value!(receiver, method_name).last(*arguments) }
34
+ when "take" then require_arity!(method_name, arguments, 1).then { enumerable_value!(receiver, method_name).take(Integer(arguments[0])) }
35
+ when "drop" then require_arity!(method_name, arguments, 1).then { enumerable_value!(receiver, method_name).drop(Integer(arguments[0])) }
36
+ when "reverse", "sort", "uniq", "compact", "flatten", "keys", "values", "min", "max", "sum"
37
+ require_arity!(method_name, arguments, 0)
38
+ safe_zero_argument_method(receiver, method_name)
39
+ when "join" then safe_join(require_array!(receiver, method_name), arguments)
40
+ when "split" then bounded_value!(require_string!(receiver, method_name).split(*arguments))
41
+ when "strip", "downcase", "upcase" then require_string!(receiver, method_name).public_send(method_name)
42
+ when "include?" then collection_value!(receiver, method_name).include?(*arguments)
43
+ when "start_with?", "end_with?" then require_string!(receiver, method_name).public_send(method_name, *arguments)
44
+ when "key?", "has_key?" then require_hash!(receiver, method_name).key?(*arguments)
45
+ when "fetch" then indexable_value!(receiver, method_name).fetch(*arguments)
46
+ when "dig" then require_hash!(receiver, method_name).dig(*arguments)
47
+ when "slice" then indexable_value!(receiver, method_name).slice(*arguments)
48
+ when "merge" then bounded_value!(require_hash!(receiver, method_name).merge(require_hash!(arguments.fetch(0), method_name)))
49
+ when "abs", "ceil", "floor", "round", "zero?", "positive?", "negative?", "even?", "odd?"
50
+ require_numeric!(receiver, method_name).public_send(method_name, *arguments)
51
+ else
52
+ invalid_receiver!(receiver, method_name)
53
+ end
54
+ rescue NoMethodError, ArgumentError, TypeError, IndexError, KeyError => error
55
+ raise CodeModeExecutionError, "Invalid #{method_name} call: #{error.message}"
56
+ end
57
+
58
+ def invoke_enumerable(receiver, method_name, arguments, block)
59
+ enumerable = enumerable_value!(receiver, method_name)
60
+ case method_name
61
+ when "map", "collect" then require_block!(method_name, block).then { enumerable.map { |*items| tick!; block.call(*block_arguments(enumerable, items)) } }
62
+ when "select", "filter" then require_block!(method_name, block).then { enumerable.select { |*items| tick!; truthy?(block.call(*block_arguments(enumerable, items))) } }
63
+ when "reject" then require_block!(method_name, block).then { enumerable.reject { |*items| tick!; truthy?(block.call(*block_arguments(enumerable, items))) } }
64
+ when "each"
65
+ require_block!(method_name, block)
66
+ enumerable.each { |*items| tick!; block.call(*block_arguments(enumerable, items)) }
67
+ receiver
68
+ when "find" then require_block!(method_name, block).then { enumerable.find { |*items| tick!; truthy?(block.call(*block_arguments(enumerable, items))) } }
69
+ when "any?" then require_block!(method_name, block).then { enumerable.any? { |*items| tick!; truthy?(block.call(*block_arguments(enumerable, items))) } }
70
+ when "all?" then require_block!(method_name, block).then { enumerable.all? { |*items| tick!; truthy?(block.call(*block_arguments(enumerable, items))) } }
71
+ when "none?" then require_block!(method_name, block).then { enumerable.none? { |*items| tick!; truthy?(block.call(*block_arguments(enumerable, items))) } }
72
+ when "count"
73
+ block ? enumerable.count { |*items| tick!; truthy?(block.call(*block_arguments(enumerable, items))) } : enumerable.count(*arguments)
74
+ when "sort_by" then require_block!(method_name, block).then { enumerable.sort_by { |*items| tick!; block.call(*block_arguments(enumerable, items)) } }
75
+ when "reduce", "inject"
76
+ reduce_enumerable(enumerable, arguments, require_block!(method_name, block))
77
+ else invalid_receiver!(receiver, method_name)
78
+ end
79
+ rescue BreakSignal => signal
80
+ signal.value
81
+ end
82
+
83
+ def block_arguments(enumerable, items)
84
+ return items.first if enumerable.is_a?(Hash) && items.length == 1 && items.first.is_a?(Array)
85
+
86
+ items
87
+ end
88
+
89
+ def reduce_enumerable(enumerable, arguments, block)
90
+ values = enumerable.to_a
91
+ if arguments.empty?
92
+ return nil if values.empty?
93
+ accumulator = values.shift
94
+ else
95
+ require_arity!("reduce", arguments, 1)
96
+ accumulator = arguments.first
97
+ end
98
+ values.each { |item| tick!; accumulator = block.call(accumulator, item) }
99
+ accumulator
100
+ end
101
+
102
+ def safe_zero_argument_method(receiver, method_name)
103
+ allowed = case receiver
104
+ when Array then %w[reverse sort uniq compact flatten min max sum]
105
+ when Hash then %w[keys values]
106
+ when Range then %w[min max sum]
107
+ else []
108
+ end
109
+ invalid_receiver!(receiver, method_name) unless allowed.include?(method_name)
110
+ bounded_value!(receiver.public_send(method_name))
111
+ end
112
+
113
+ def safe_to_a(receiver)
114
+ return receiver.dup if receiver.is_a?(Array)
115
+ if receiver.is_a?(Range)
116
+ if receiver.begin.is_a?(Integer) && receiver.end.is_a?(Integer)
117
+ size = receiver.end - receiver.begin + (receiver.exclude_end? ? 0 : 1)
118
+ raise CodeModeLimitError, "Range contains too many values" if size > MAX_VALUE_ITEMS
119
+ end
120
+ return bounded_value!(receiver.to_a)
121
+ end
122
+ return receiver.to_a if receiver.is_a?(Hash)
123
+
124
+ invalid_receiver!(receiver, "to_a")
125
+ end
126
+
127
+ def safe_index(receiver, arguments)
128
+ raise CodeModeSyntaxError, "Indexing requires one or two arguments" unless (1..2).cover?(arguments.length)
129
+ raise CodeModeExecutionError, "#{receiver.class} values cannot be indexed" unless receiver.is_a?(Array) || receiver.is_a?(Hash) || receiver.is_a?(String)
130
+
131
+ receiver[*arguments]
132
+ rescue TypeError, IndexError => error
133
+ raise CodeModeExecutionError, "Invalid index: #{error.message}"
134
+ end
135
+
136
+ def enumerable_value!(receiver, method_name)
137
+ return receiver if receiver.is_a?(Array) || receiver.is_a?(Hash) || receiver.is_a?(Range)
138
+
139
+ invalid_receiver!(receiver, method_name)
140
+ end
141
+
142
+ def require_string!(value, method_name)
143
+ return value if value.is_a?(String)
144
+
145
+ invalid_receiver!(value, method_name)
146
+ end
147
+
148
+ def require_hash!(value, method_name)
149
+ return value if value.is_a?(Hash)
150
+
151
+ invalid_receiver!(value, method_name)
152
+ end
153
+
154
+ def require_array!(value, method_name)
155
+ return value if value.is_a?(Array)
156
+
157
+ invalid_receiver!(value, method_name)
158
+ end
159
+
160
+ def collection_value!(value, method_name)
161
+ return value if value.is_a?(Array) || value.is_a?(Hash) || value.is_a?(String) || value.is_a?(Range)
162
+
163
+ invalid_receiver!(value, method_name)
164
+ end
165
+
166
+ def indexable_value!(value, method_name)
167
+ return value if value.is_a?(Array) || value.is_a?(Hash) || value.is_a?(String)
168
+
169
+ invalid_receiver!(value, method_name)
170
+ end
171
+
172
+ def require_numeric!(value, method_name)
173
+ return value if value.is_a?(Numeric)
174
+
175
+ invalid_receiver!(value, method_name)
176
+ end
177
+
178
+ def safe_join(receiver, arguments)
179
+ separator = arguments.empty? ? "" : arguments.first.to_s
180
+ estimated = receiver.sum { |value| value.to_s.bytesize }
181
+ estimated += separator.bytesize * [receiver.length - 1, 0].max
182
+ raise CodeModeLimitError, "String result exceeds #{MAX_VALUE_BYTES} bytes" if estimated > MAX_VALUE_BYTES
183
+
184
+ receiver.join(*arguments)
185
+ end
186
+
187
+ def invalid_receiver!(receiver, method_name)
188
+ raise CodeModeExecutionError, "#{method_name} is not supported for #{receiver.class} values"
189
+ end
190
+
191
+ def require_block!(method_name, block)
192
+ raise CodeModeSyntaxError, "#{method_name} requires a block" unless block
193
+ block
194
+ end
195
+
196
+ def require_arity!(name, arguments, expected)
197
+ valid = expected.is_a?(Range) ? expected.cover?(arguments.length) : arguments.length == expected
198
+ return true if valid
199
+
200
+ raise CodeModeSyntaxError, "#{name} received #{arguments.length} arguments"
201
+ end
202
+ end
203
+ end
204
+ end