ruby-utcp 1.1.0 → 1.1.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4a695abfcf37112217be77067218db124af0ea02490ea1811ecafc5f6cf2ab30
4
- data.tar.gz: 8ef4eb2d484d370820f9c10cfc95c00c36e66675770e49ac848790ba3bf1678c
3
+ metadata.gz: 99c878fb7098dd9c213ffa10c01349acf87fb03f6e5742f9214ec077a3502c0b
4
+ data.tar.gz: 0dfd532e70fde83d025aa0a9c6282c460a26b2c333cb917e6951d62ac979c780
5
5
  SHA512:
6
- metadata.gz: 703e659df46fcd3574bd377debd7623b808e4b57334da07a9a6a28461ad25ad5468b97e81aff53b066178de43184f24117a0ccd7ab75d97241cbaabf0dd26574
7
- data.tar.gz: 0661067b9c78f93f140a130d7e4d21dc1c1debe50e0dcbfcddee1c70e5bb67186b14d4a57433a29b61505fed4e85dcf77a1a0dfd38a7cb669fb784867bccdf36
6
+ metadata.gz: fa88cea89f5be786a37ba117ee308e301d760d55b76d9d409be03a1370934cc6b11a534d80b16d5c2c335d0dcc4ea82358ee7dda6a6eaaff768a2f7ecf8f7f68
7
+ data.tar.gz: 6eeb7f901ca0e58de3d9f33f8ca92bf990ea8e547aad89e5b28a17a7337ed491ceef0bf747710fa81c26092706550ebb0407e849e38e1ed3ee0d6dbfb30c312e
data/Makefile CHANGED
@@ -18,6 +18,7 @@ STANDARD_SERVER_SCRIPTS := \
18
18
 
19
19
  STANDARD_CLIENT_SCRIPTS := \
20
20
  examples/http.rb \
21
+ examples/code_mode.rb \
21
22
  examples/sse.rb \
22
23
  examples/streamable_http.rb \
23
24
  examples/cli.rb \
data/README.md CHANGED
@@ -289,6 +289,28 @@ client.search_tools("weather forecast", limit: 5, any_of_tags_required: ["weathe
289
289
 
290
290
  Pass objects that implement the repository or search interfaces through `tool_repository` and `tool_search_strategy` to replace the defaults.
291
291
 
292
+ ## Code Mode
293
+
294
+ `CodeModeUtcpClient` can run a multi-step Ruby workflow as one call. Tools are invoked through the explicit `codemode` runtime API, and the final expression (or an explicit `return`) becomes the result:
295
+
296
+ ```ruby
297
+ client = UTCP::CodeModeUtcpClient.create(config: config)
298
+
299
+ execution = client.call_tool_chain(<<~'RUBY', timeout: 30)
300
+ weather = codemode.call_tool("weather_service.get_weather", location: "Warsaw")
301
+ alerts = weather["alerts"].select { |alert| alert["severity"] == "high" }
302
+ puts "Found #{alerts.length} high-severity alerts"
303
+ { temperature: weather["temperature"], alerts: alerts }
304
+ RUBY
305
+
306
+ puts execution["result"]
307
+ puts execution["logs"]
308
+ ```
309
+
310
+ Use `codemode.call_tool_stream` to collect a streaming call into an array. `codemode.search_tools`, `codemode.get_tool_interface`, and `codemode.interfaces` provide progressive discovery inside a workflow. `codemode.get(value, key, default)` safely reads dynamic results, and `get_all_tools_ruby_interfaces` provides the tool catalog outside the sandbox.
311
+
312
+ Code Mode accepts a constrained Ruby subset for local variables, JSON-like literals, arithmetic, conditionals, loops, indexing, and common collection transforms. It is interpreted without `eval`; filesystem, process, constant, reflection, import, and direct network APIs are not exposed. Executions also have code-size, step, result-size, log-size, and wall-clock limits. External effects remain possible through the UTCP tools that you deliberately register.
313
+
292
314
  ## Custom protocol plugins
293
315
 
294
316
  Register a call-template class and a protocol implementation before creating a client:
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "utcp"
5
+
6
+ # Reuses examples/servers/http_server.rb so Code Mode can focus on orchestration.
7
+ client = UTCP::CodeModeUtcpClient.create(config: {
8
+ manual_call_templates: [{
9
+ name: "rest",
10
+ call_template_type: "http",
11
+ url: ENV.fetch("UTCP_HTTP_MANUAL", "http://localhost:8080/utcp"),
12
+ http_method: "GET"
13
+ }]
14
+ })
15
+
16
+ execution = client.call_tool_chain(<<~'RUBY', timeout: 10)
17
+ matches = codemode.search_tools("echo", limit: 1)
18
+ tool_name = matches.first["name"]
19
+
20
+ first = codemode.call_tool(tool_name, body: { message: "Hello from Code Mode" })
21
+ second = codemode.call_tool(tool_name, body: { message: first["message"].upcase })
22
+
23
+ messages = [first, second].map { |response| response["message"] }
24
+ puts "Called #{tool_name} #{messages.length} times"
25
+
26
+ { tool: tool_name, messages: messages }
27
+ RUBY
28
+
29
+ puts JSON.pretty_generate(execution)
@@ -0,0 +1,937 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "ripper"
5
+ require "timeout"
6
+
7
+ module UTCP
8
+ # Executes a deliberately small, non-eval Ruby subset for composing UTCP tools.
9
+ # The interpreter only exposes JSON-like values and explicit tool helpers.
10
+ class CodeMode
11
+ DEFAULT_TIMEOUT = 30
12
+ DEFAULT_MAX_STEPS = 100_000
13
+ MAX_CODE_BYTES = 64 * 1024
14
+ MAX_LOG_BYTES = 1024 * 1024
15
+ MAX_VALUE_BYTES = 1024 * 1024
16
+ MAX_VALUE_ITEMS = 10_000
17
+
18
+ attr_reader :client
19
+
20
+ def initialize(client)
21
+ @client = client
22
+ end
23
+
24
+ def execute(code, timeout: DEFAULT_TIMEOUT, max_steps: DEFAULT_MAX_STEPS)
25
+ source = String(code)
26
+ raise CodeModeLimitError, "Code exceeds #{MAX_CODE_BYTES} bytes" if source.bytesize > MAX_CODE_BYTES
27
+
28
+ seconds = Float(timeout)
29
+ raise CodeModeLimitError, "timeout must be greater than zero" unless seconds.positive?
30
+
31
+ evaluator = Evaluator.new(
32
+ client,
33
+ interfaces: interfaces,
34
+ timeout: seconds,
35
+ max_steps: Integer(max_steps)
36
+ )
37
+ result = Timeout.timeout(seconds, CodeModeTimeoutError) { evaluator.execute(source) }
38
+ { "result" => result, "logs" => evaluator.logs.dup }
39
+ rescue CodeModeTimeoutError
40
+ raise CodeModeTimeoutError, "Code Mode execution exceeded #{timeout} seconds"
41
+ rescue CodeModeExecutionError => error
42
+ logs = defined?(evaluator) && evaluator ? evaluator.logs : error.logs
43
+ raise CodeModeExecutionError.new(error.message, logs: logs)
44
+ rescue CodeModeError
45
+ raise
46
+ rescue StandardError => error
47
+ logs = defined?(evaluator) && evaluator ? evaluator.logs : []
48
+ raise CodeModeExecutionError.new("Code Mode execution failed: #{error.message}", logs: logs)
49
+ end
50
+
51
+ def interfaces
52
+ InterfaceGenerator.new(client.list_tools).render
53
+ end
54
+
55
+ def tool_interface(name)
56
+ tool = client.list_tools.find { |candidate| candidate.name == name.to_s }
57
+ raise ToolNotFoundError, "Tool not found: #{name}" unless tool
58
+
59
+ InterfaceGenerator.tool_descriptor(tool)
60
+ end
61
+
62
+ class InterfaceGenerator
63
+ RUBY_KEYWORDS = %w[
64
+ BEGIN END alias and begin break case class def defined? do else elsif end ensure false
65
+ for if in module next nil not or redo rescue retry return self super then true undef
66
+ unless until when while yield
67
+ ].freeze
68
+
69
+ class << self
70
+ def tool_descriptor(tool)
71
+ {
72
+ "name" => tool.name,
73
+ "description" => tool.description,
74
+ "inputs" => tool.inputs.to_h,
75
+ "outputs" => tool.outputs.to_h,
76
+ "tags" => tool.tags.dup
77
+ }
78
+ end
79
+
80
+ def identifier(value)
81
+ name = value.to_s.gsub(/[^a-zA-Z0-9_]/, "_")
82
+ name = "_#{name}" if name.match?(/\A\d/)
83
+ name = "_#{name}" if RUBY_KEYWORDS.include?(name)
84
+ name.empty? ? "_" : name
85
+ end
86
+
87
+ def ruby_type(schema)
88
+ value = schema.is_a?(Hash) ? schema : {}
89
+ case value["type"] || value[:type]
90
+ when "string" then "String"
91
+ when "integer" then "Integer"
92
+ when "number" then "Numeric"
93
+ when "boolean" then "Boolean"
94
+ when "array" then "Array"
95
+ when "object" then "Hash"
96
+ when Array then (value["type"] || value[:type]).map { |type| ruby_type("type" => type) }.join(" | ")
97
+ else "Object"
98
+ end
99
+ end
100
+ end
101
+
102
+ def initialize(tools)
103
+ @tools = tools.sort_by(&:name)
104
+ end
105
+
106
+ def render
107
+ return "# No UTCP tools are registered." if @tools.empty?
108
+
109
+ @tools.map { |tool| render_tool(tool) }.join("\n\n")
110
+ end
111
+
112
+ private
113
+
114
+ def render_tool(tool)
115
+ descriptor = self.class.tool_descriptor(tool)
116
+ schema = descriptor["inputs"]
117
+ properties = schema.fetch("properties", {})
118
+ required = Array(schema["required"])
119
+ parameters = properties.map do |name, property|
120
+ suffix = required.include?(name.to_s) ? ":" : ": nil"
121
+ "#{self.class.identifier(name)}#{suffix}"
122
+ end
123
+ signature = parameters.empty? ? "" : parameters.join(", ")
124
+ description = descriptor["description"].to_s.strip
125
+ lines = []
126
+ lines << "# #{description}" unless description.empty?
127
+ unless properties.empty?
128
+ parameter_docs = properties.map do |name, property|
129
+ requirement = required.include?(name.to_s) ? "required" : "optional"
130
+ "#{self.class.identifier(name)} (#{self.class.ruby_type(property)}, #{requirement})"
131
+ end
132
+ lines << "# Parameters: #{parameter_docs.join(', ')}"
133
+ end
134
+ lines << "# Returns: #{self.class.ruby_type(descriptor["outputs"])}"
135
+ separator = signature.empty? ? "" : ", "
136
+ lines << "codemode.call_tool(#{descriptor["name"].inspect}#{separator}#{signature})"
137
+ lines.join("\n")
138
+ end
139
+ end
140
+
141
+ class Evaluator
142
+ ReturnSignal = Class.new(StandardError) do
143
+ attr_reader :value
144
+
145
+ def initialize(value)
146
+ @value = value
147
+ super()
148
+ end
149
+ end
150
+ BreakSignal = Class.new(StandardError) do
151
+ attr_reader :value
152
+
153
+ def initialize(value = nil)
154
+ @value = value
155
+ super()
156
+ end
157
+ end
158
+ NextSignal = Class.new(StandardError) do
159
+ attr_reader :value
160
+
161
+ def initialize(value = nil)
162
+ @value = value
163
+ super()
164
+ end
165
+ end
166
+
167
+ RuntimeAPI = Class.new
168
+ Block = Struct.new(:evaluator, :parameters, :body) do
169
+ def call(*values)
170
+ scope = {}
171
+ if parameters.length == 1 && values.length > 1
172
+ scope[parameters.first] = values
173
+ else
174
+ parameters.each_with_index { |name, index| scope[name] = values[index] }
175
+ end
176
+ evaluator.with_scope(scope) { evaluator.evaluate_statements(body) }
177
+ rescue NextSignal => signal
178
+ signal.value
179
+ end
180
+ end
181
+
182
+ SAFE_ENUMERABLE_METHODS = %w[
183
+ all? any? collect count each filter find map none? reduce inject reject select sort_by
184
+ ].freeze
185
+ SAFE_VALUE_METHODS = %w[
186
+ abs ceil compact dig downcase drop empty? end_with? even? fetch first flatten floor
187
+ has_key? include? inspect join key? keys last length max merge min negative? odd? positive?
188
+ nil? reverse round size slice sort split start_with? strip sum take to_a to_f to_h to_i to_s
189
+ uniq upcase values zero?
190
+ ].freeze
191
+
192
+ attr_reader :logs
193
+
194
+ def initialize(client, interfaces:, timeout:, max_steps:)
195
+ raise CodeModeLimitError, "max_steps must be greater than zero" unless max_steps.positive?
196
+
197
+ @client = client
198
+ @interfaces = interfaces
199
+ @deadline = monotonic_now + timeout
200
+ @max_steps = max_steps
201
+ @steps = 0
202
+ @logs = []
203
+ @log_bytes = 0
204
+ @scopes = [{ "codemode" => RuntimeAPI.new }]
205
+ end
206
+
207
+ def execute(source)
208
+ tree = Ripper.sexp(source)
209
+ raise CodeModeSyntaxError, "Invalid Ruby syntax" unless tree
210
+
211
+ safe_tool_value(evaluate(tree))
212
+ rescue ReturnSignal => signal
213
+ safe_tool_value(signal.value)
214
+ end
215
+
216
+ def evaluate(node)
217
+ tick!
218
+ return nil if node.nil?
219
+ return node unless node.is_a?(Array)
220
+ return evaluate_statements(node) unless node.first.is_a?(Symbol)
221
+
222
+ type = node.first
223
+ case type
224
+ when :program then evaluate_statements(node[1])
225
+ when :void_stmt then nil
226
+ when :assign then assign(node[1], evaluate(node[2]))
227
+ when :opassign then evaluate_opassign(node)
228
+ when :var_ref then evaluate_variable_token(node[1])
229
+ when :vcall then evaluate_vcall(node[1])
230
+ when :fcall then call_function(token_value(node[1]), [], nil)
231
+ when :@int then Integer(node[1], 10)
232
+ when :@float then Float(node[1])
233
+ when :@kw then evaluate_keyword(node[1])
234
+ when :string_literal then evaluate(node[1]).to_s
235
+ when :string_content then node.drop(1).map { |part| evaluate_string_part(part) }.join
236
+ when :symbol_literal then evaluate_symbol(node[1])
237
+ when :array then Array(node[1]).map { |item| evaluate(item) }
238
+ when :hash then evaluate_hash(node[1])
239
+ when :bare_assoc_hash then evaluate_associations(node[1])
240
+ when :paren then evaluate_statements(node[1])
241
+ when :binary then evaluate_binary(node[1], node[2], node[3])
242
+ when :unary then evaluate_unary(node[1], node[2])
243
+ when :dot2 then Range.new(evaluate(node[1]), evaluate(node[2]), false)
244
+ when :dot3 then Range.new(evaluate(node[1]), evaluate(node[2]), true)
245
+ when :aref then safe_index(evaluate(node[1]), extract_arguments(node[2]))
246
+ when :call, :method_add_arg, :command, :command_call then evaluate_call(node)
247
+ when :method_add_block then evaluate_call(node[1], build_block(node[2]))
248
+ when :begin then evaluate(node[1])
249
+ when :bodystmt then evaluate_body_statement(node)
250
+ when :rescue_mod then evaluate_rescue_modifier(node)
251
+ when :if then evaluate_if(node[1], node[2], node[3])
252
+ when :unless then evaluate_unless(node[1], node[2], node[3])
253
+ when :if_mod then truthy?(evaluate(node[1])) ? evaluate(node[2]) : nil
254
+ when :unless_mod then truthy?(evaluate(node[1])) ? nil : evaluate(node[2])
255
+ when :while then evaluate_loop(node[1], node[2], until_condition: false)
256
+ when :until then evaluate_loop(node[1], node[2], until_condition: true)
257
+ when :return then raise ReturnSignal, return_value(node[1])
258
+ when :return0 then raise ReturnSignal, nil
259
+ when :break then raise BreakSignal, return_value(node[1])
260
+ when :next then raise NextSignal, return_value(node[1])
261
+ else
262
+ unsupported!(node)
263
+ end
264
+ end
265
+
266
+ def evaluate_statements(statements)
267
+ Array(statements).reduce(nil) { |_result, statement| evaluate(statement) }
268
+ end
269
+
270
+ def with_scope(scope)
271
+ @scopes << scope
272
+ yield
273
+ ensure
274
+ @scopes.pop
275
+ end
276
+
277
+ private
278
+
279
+ def tick!
280
+ @steps += 1
281
+ raise CodeModeLimitError, "Code Mode step limit exceeded" if @steps > @max_steps
282
+ raise CodeModeTimeoutError, "Code Mode execution timed out" if monotonic_now >= @deadline
283
+ end
284
+
285
+ def monotonic_now
286
+ Process.clock_gettime(Process::CLOCK_MONOTONIC)
287
+ end
288
+
289
+ def unsupported!(node)
290
+ line = find_line(node)
291
+ suffix = line ? " at line #{line}" : ""
292
+ raise CodeModeSyntaxError, "Unsupported Ruby construct #{node.first.inspect}#{suffix}"
293
+ end
294
+
295
+ def find_line(node)
296
+ return node[2][0] if node.is_a?(Array) && node.first.to_s.start_with?("@") && node[2].is_a?(Array)
297
+ return nil unless node.is_a?(Array)
298
+
299
+ node.each do |child|
300
+ line = find_line(child)
301
+ return line if line
302
+ end
303
+ nil
304
+ end
305
+
306
+ def token_value(token)
307
+ token.is_a?(Array) ? token[1].to_s : token.to_s
308
+ end
309
+
310
+ def evaluate_keyword(keyword)
311
+ case keyword
312
+ when "true" then true
313
+ when "false" then false
314
+ when "nil" then nil
315
+ else raise CodeModeSyntaxError, "Unsupported keyword #{keyword.inspect}"
316
+ end
317
+ end
318
+
319
+ def evaluate_string_part(part)
320
+ return part[1] if part.first == :@tstring_content
321
+ return evaluate_statements(part[1]).to_s if part.first == :string_embexpr
322
+
323
+ unsupported!(part)
324
+ end
325
+
326
+ def evaluate_symbol(node)
327
+ token = node.first == :symbol ? node[1] : node
328
+ token_value(token).to_sym
329
+ end
330
+
331
+ def evaluate_hash(contents)
332
+ return {} unless contents
333
+ return evaluate_associations(contents[1]) if contents.first == :assoclist_from_args
334
+
335
+ unsupported!(contents)
336
+ end
337
+
338
+ def evaluate_associations(associations)
339
+ Array(associations).each_with_object({}) do |association, result|
340
+ unsupported!(association) unless association.first == :assoc_new
341
+ key_node = association[1]
342
+ key = key_node.first == :@label ? key_node[1].sub(/:\z/, "") : evaluate(key_node)
343
+ result[key] = evaluate(association[2])
344
+ end
345
+ end
346
+
347
+ def evaluate_variable_token(token)
348
+ return evaluate_keyword(token[1]) if token.first == :@kw
349
+ lookup(token_value(token))
350
+ end
351
+
352
+ def evaluate_vcall(token)
353
+ name = token_value(token)
354
+ value = lookup(name, missing: :sentinel)
355
+ return value unless value == :sentinel
356
+
357
+ call_function(name, [], nil)
358
+ end
359
+
360
+ def lookup(name, missing: nil)
361
+ @scopes.reverse_each { |scope| return scope[name] if scope.key?(name) }
362
+ return missing if missing == :sentinel
363
+
364
+ raise CodeModeExecutionError, "Undefined local variable #{name.inspect}"
365
+ end
366
+
367
+ def assign(target, value)
368
+ case target.first
369
+ when :var_field
370
+ name = token_value(target[1])
371
+ scope = @scopes.reverse.find { |candidate| candidate.key?(name) } || @scopes.last
372
+ scope[name] = value
373
+ when :aref_field
374
+ receiver = evaluate(target[1])
375
+ arguments = extract_arguments(target[2])
376
+ raise CodeModeSyntaxError, "Indexed assignment requires exactly one key" unless arguments.length == 1
377
+ raise CodeModeSyntaxError, "Indexed assignment is only allowed on arrays and hashes" unless receiver.is_a?(Array) || receiver.is_a?(Hash)
378
+
379
+ receiver[arguments.first] = value
380
+ else
381
+ unsupported!(target)
382
+ end
383
+ value
384
+ end
385
+
386
+ def evaluate_opassign(node)
387
+ target = node[1]
388
+ operator = token_value(node[2]).sub(/=\z/, "").to_sym
389
+ current = read_assignment_target(target)
390
+ assign(target, apply_binary(current, operator, evaluate(node[3])))
391
+ end
392
+
393
+ def read_assignment_target(target)
394
+ case target.first
395
+ when :var_field then lookup(token_value(target[1]))
396
+ when :aref_field then safe_index(evaluate(target[1]), extract_arguments(target[2]))
397
+ else unsupported!(target)
398
+ end
399
+ end
400
+
401
+ def evaluate_binary(left_node, operator, right_node)
402
+ left = evaluate(left_node)
403
+ return left unless truthy?(left) if [:'&&', :and].include?(operator)
404
+ return left if truthy?(left) if [:'||', :or].include?(operator)
405
+
406
+ apply_binary(left, operator, evaluate(right_node))
407
+ end
408
+
409
+ def apply_binary(left, operator, right)
410
+ preflight_binary_size!(left, operator, right)
411
+ value = case operator
412
+ when :+ then left + right
413
+ when :- then left - right
414
+ when :* then left * right
415
+ when :/ then left / right
416
+ when :% then left % right
417
+ when :** then left**right
418
+ when :== then left == right
419
+ when :!= then left != right
420
+ when :< then left < right
421
+ when :<= then left <= right
422
+ when :> then left > right
423
+ when :>= then left >= right
424
+ when :<=> then left <=> right
425
+ when :'&&', :and, :'||', :or then right
426
+ else raise CodeModeSyntaxError, "Unsupported operator #{operator.inspect}"
427
+ end
428
+ bounded_value!(value)
429
+ rescue NoMethodError, TypeError, ArgumentError, ZeroDivisionError => error
430
+ raise CodeModeExecutionError, "Invalid #{operator} operation: #{error.message}"
431
+ end
432
+
433
+ def evaluate_unary(operator, operand_node)
434
+ operand = evaluate(operand_node)
435
+ case operator
436
+ when :! then !truthy?(operand)
437
+ when :+@ then +operand
438
+ when :-@ then -operand
439
+ else raise CodeModeSyntaxError, "Unsupported unary operator #{operator.inspect}"
440
+ end
441
+ end
442
+
443
+ def truthy?(value)
444
+ !value.nil? && value != false
445
+ end
446
+
447
+ def evaluate_if(condition, truthy_statements, alternative)
448
+ if truthy?(evaluate(condition))
449
+ evaluate_statements(truthy_statements)
450
+ elsif alternative&.first == :else
451
+ evaluate_statements(alternative[1])
452
+ elsif alternative&.first == :elsif
453
+ evaluate_if(alternative[1], alternative[2], alternative[3])
454
+ end
455
+ end
456
+
457
+ def evaluate_unless(condition, statements, alternative)
458
+ unless truthy?(evaluate(condition))
459
+ evaluate_statements(statements)
460
+ else
461
+ evaluate_statements(alternative[1]) if alternative&.first == :else
462
+ end
463
+ end
464
+
465
+ def evaluate_body_statement(node)
466
+ statements, rescue_clause, else_clause, ensure_clause = node.drop(1)
467
+ completed = false
468
+ result = begin
469
+ value = evaluate_statements(statements)
470
+ completed = true
471
+ value
472
+ rescue ReturnSignal, BreakSignal, NextSignal, CodeModeTimeoutError, CodeModeLimitError, CodeModeSyntaxError
473
+ raise
474
+ rescue StandardError => error
475
+ raise unless rescue_clause
476
+ evaluate_rescue_clause(rescue_clause, error)
477
+ ensure
478
+ evaluate_statements(ensure_clause[1]) if ensure_clause&.first == :ensure
479
+ end
480
+ completed && else_clause ? evaluate_statements(else_clause) : result
481
+ end
482
+
483
+ def evaluate_rescue_clause(node, error)
484
+ unsupported!(node) unless node.first == :rescue
485
+ raise CodeModeSyntaxError, "Code Mode rescue does not accept exception classes" if node[1]
486
+
487
+ scope = {}
488
+ assign_rescue_variable(scope, node[2], error) if node[2]
489
+ with_scope(scope) { evaluate_statements(node[3]) }
490
+ rescue ReturnSignal, BreakSignal, NextSignal, CodeModeTimeoutError, CodeModeLimitError, CodeModeSyntaxError
491
+ raise
492
+ rescue StandardError => nested
493
+ next_clause = node[4]
494
+ raise nested unless next_clause&.first == :rescue
495
+ evaluate_rescue_clause(next_clause, nested)
496
+ end
497
+
498
+ def assign_rescue_variable(scope, target, error)
499
+ unsupported!(target) unless target.first == :var_field
500
+ scope[token_value(target[1])] = {
501
+ "message" => error.message,
502
+ "type" => error.class.name
503
+ }
504
+ end
505
+
506
+ def evaluate_rescue_modifier(node)
507
+ evaluate(node[1])
508
+ rescue ReturnSignal, BreakSignal, NextSignal, CodeModeTimeoutError, CodeModeLimitError, CodeModeSyntaxError
509
+ raise
510
+ rescue StandardError
511
+ evaluate(node[2])
512
+ end
513
+
514
+ def evaluate_loop(condition, statements, until_condition:)
515
+ result = nil
516
+ loop do
517
+ matches = truthy?(evaluate(condition))
518
+ break if until_condition ? matches : !matches
519
+
520
+ begin
521
+ result = evaluate_statements(statements)
522
+ rescue NextSignal => signal
523
+ result = signal.value
524
+ rescue BreakSignal => signal
525
+ return signal.value
526
+ end
527
+ end
528
+ result
529
+ end
530
+
531
+ def return_value(arguments_node)
532
+ arguments = extract_arguments(arguments_node)
533
+ arguments.length <= 1 ? arguments.first : arguments
534
+ end
535
+
536
+ def extract_arguments(node)
537
+ return [] if node.nil?
538
+ case node.first
539
+ when :arg_paren then extract_arguments(node[1])
540
+ when :args_add_block then Array(node[1]).map { |argument| evaluate(argument) }
541
+ when :args_new then []
542
+ when :args_add then extract_arguments(node[1]) + [evaluate(node[2])]
543
+ else [evaluate(node)]
544
+ end
545
+ end
546
+
547
+ def build_block(node)
548
+ unsupported!(node) unless %i[brace_block do_block].include?(node.first)
549
+ parameters = extract_block_parameters(node[1])
550
+ Block.new(self, parameters, node[2])
551
+ end
552
+
553
+ def extract_block_parameters(node)
554
+ return [] unless node
555
+ params = node.first == :block_var ? node[1] : node
556
+ unsupported!(params) unless params&.first == :params
557
+ required = Array(params[1])
558
+ unsupported!(params) unless params.drop(2).all?(&:nil?)
559
+
560
+ required.map { |token| token_value(token) }
561
+ end
562
+
563
+ def evaluate_call(node, block = nil)
564
+ case node.first
565
+ when :method_add_arg
566
+ invoke_call_target(node[1], extract_arguments(node[2]), block)
567
+ when :call
568
+ invoke_method(evaluate(node[1]), token_value(node[3]), [], block)
569
+ when :fcall
570
+ call_function(token_value(node[1]), [], block)
571
+ when :vcall
572
+ evaluate_vcall(node[1])
573
+ when :command
574
+ call_function(token_value(node[1]), extract_arguments(node[2]), block)
575
+ when :command_call
576
+ invoke_method(evaluate(node[1]), token_value(node[3]), extract_arguments(node[4]), block)
577
+ else
578
+ unsupported!(node)
579
+ end
580
+ end
581
+
582
+ def invoke_call_target(target, arguments, block)
583
+ case target.first
584
+ when :call
585
+ invoke_method(evaluate(target[1]), token_value(target[3]), arguments, block)
586
+ when :fcall
587
+ call_function(token_value(target[1]), arguments, block)
588
+ when :vcall
589
+ call_function(token_value(target[1]), arguments, block)
590
+ else
591
+ unsupported!(target)
592
+ end
593
+ end
594
+
595
+ def call_function(name, arguments, block)
596
+ case name
597
+ when "puts", "print", "p", "warn"
598
+ log(name, arguments)
599
+ nil
600
+ else
601
+ hint = name == "call_tool" ? "; use codemode.call_tool(...)" : ""
602
+ raise CodeModeSyntaxError, "Function #{name.inspect} is not available in Code Mode#{hint}"
603
+ end
604
+ end
605
+
606
+ def invoke_method(receiver, method_name, arguments, block)
607
+ return invoke_runtime_api(method_name, arguments, block) if receiver.is_a?(RuntimeAPI)
608
+ return receiver[method_name] if receiver.is_a?(Hash) && arguments.empty? && !block && receiver.key?(method_name)
609
+
610
+ unless SAFE_ENUMERABLE_METHODS.include?(method_name) || SAFE_VALUE_METHODS.include?(method_name)
611
+ raise CodeModeSyntaxError, "Method #{method_name.inspect} is not available in Code Mode"
612
+ end
613
+
614
+ invoke_safe_value_method(receiver, method_name, arguments, block)
615
+ end
616
+
617
+ def invoke_runtime_api(method_name, arguments, block)
618
+ raise CodeModeSyntaxError, "codemode.#{method_name} does not accept a block" if block
619
+
620
+ case method_name
621
+ when "call_tool"
622
+ require_arity!("codemode.call_tool", arguments, 1..2)
623
+ safe_tool_value(@client.call_tool(arguments[0], tool_arguments(arguments[1])))
624
+ when "call_tool_stream", "call_tool_streaming"
625
+ require_arity!("codemode.#{method_name}", arguments, 1..2)
626
+ safe_tool_value(@client.call_tool_streaming(arguments[0], tool_arguments(arguments[1])).to_a)
627
+ when "search_tools"
628
+ require_arity!("codemode.search_tools", arguments, 1..2)
629
+ options = arguments[1].is_a?(Hash) ? arguments[1] : {}
630
+ limit = arguments[1].is_a?(Numeric) ? arguments[1] : options.fetch("limit", 10)
631
+ safe_tool_value(@client.search_tools(arguments[0].to_s, limit: Integer(limit)).map do |tool|
632
+ InterfaceGenerator.tool_descriptor(tool)
633
+ end)
634
+ when "get_tool_interface"
635
+ require_arity!("codemode.get_tool_interface", arguments, 1)
636
+ tool = @client.list_tools.find { |candidate| candidate.name == arguments.first.to_s }
637
+ raise ToolNotFoundError, "Tool not found: #{arguments.first}" unless tool
638
+ safe_tool_value(InterfaceGenerator.tool_descriptor(tool))
639
+ when "interfaces"
640
+ require_arity!("codemode.interfaces", arguments, 0)
641
+ @interfaces.dup
642
+ when "get"
643
+ runtime_get(arguments)
644
+ else
645
+ raise CodeModeSyntaxError, "Method #{method_name.inspect} is not available on codemode"
646
+ end
647
+ end
648
+
649
+ def runtime_get(arguments)
650
+ require_arity!("codemode.get", arguments, 2..3)
651
+ receiver, key, default = arguments
652
+ return receiver.fetch(key, default) if receiver.is_a?(Hash)
653
+ return receiver.fetch(Integer(key), default) if receiver.is_a?(Array)
654
+ return receiver[key] || default if receiver.is_a?(String)
655
+
656
+ raise CodeModeExecutionError, "codemode.get requires a hash, array, or string"
657
+ rescue IndexError, TypeError
658
+ default
659
+ end
660
+
661
+ def invoke_safe_value_method(receiver, method_name, arguments, block)
662
+ return invoke_enumerable(receiver, method_name, arguments, block) if SAFE_ENUMERABLE_METHODS.include?(method_name)
663
+
664
+ case method_name
665
+ when "length", "size" then require_arity!(method_name, arguments, 0).then { receiver.respond_to?(:length) ? receiver.length : invalid_receiver!(receiver, method_name) }
666
+ when "empty?" then require_arity!(method_name, arguments, 0).then { receiver.respond_to?(:empty?) ? receiver.empty? : invalid_receiver!(receiver, method_name) }
667
+ when "nil?" then require_arity!(method_name, arguments, 0).then { receiver.nil? }
668
+ when "to_s" then require_arity!(method_name, arguments, 0).then { receiver.to_s }
669
+ when "inspect" then require_arity!(method_name, arguments, 0).then { receiver.inspect }
670
+ when "to_i" then require_arity!(method_name, arguments, 0).then { receiver.to_i }
671
+ when "to_f" then require_arity!(method_name, arguments, 0).then { receiver.to_f }
672
+ when "to_a" then require_arity!(method_name, arguments, 0).then { safe_to_a(receiver) }
673
+ when "to_h" then require_arity!(method_name, arguments, 0).then { receiver.is_a?(Hash) ? receiver.dup : invalid_receiver!(receiver, method_name) }
674
+ when "first" then require_arity!(method_name, arguments, 0..1).then { enumerable_value!(receiver, method_name).first(*arguments) }
675
+ when "last" then require_arity!(method_name, arguments, 0..1).then { enumerable_value!(receiver, method_name).last(*arguments) }
676
+ when "take" then require_arity!(method_name, arguments, 1).then { enumerable_value!(receiver, method_name).take(Integer(arguments[0])) }
677
+ when "drop" then require_arity!(method_name, arguments, 1).then { enumerable_value!(receiver, method_name).drop(Integer(arguments[0])) }
678
+ when "reverse", "sort", "uniq", "compact", "flatten", "keys", "values", "min", "max", "sum"
679
+ require_arity!(method_name, arguments, 0)
680
+ safe_zero_argument_method(receiver, method_name)
681
+ when "join" then safe_join(require_array!(receiver, method_name), arguments)
682
+ when "split" then bounded_value!(require_string!(receiver, method_name).split(*arguments))
683
+ when "strip", "downcase", "upcase" then require_string!(receiver, method_name).public_send(method_name)
684
+ when "include?" then collection_value!(receiver, method_name).include?(*arguments)
685
+ when "start_with?", "end_with?" then require_string!(receiver, method_name).public_send(method_name, *arguments)
686
+ when "key?", "has_key?" then require_hash!(receiver, method_name).key?(*arguments)
687
+ when "fetch" then indexable_value!(receiver, method_name).fetch(*arguments)
688
+ when "dig" then require_hash!(receiver, method_name).dig(*arguments)
689
+ when "slice" then indexable_value!(receiver, method_name).slice(*arguments)
690
+ when "merge" then bounded_value!(require_hash!(receiver, method_name).merge(require_hash!(arguments.fetch(0), method_name)))
691
+ when "abs", "ceil", "floor", "round", "zero?", "positive?", "negative?", "even?", "odd?"
692
+ require_numeric!(receiver, method_name).public_send(method_name, *arguments)
693
+ else
694
+ invalid_receiver!(receiver, method_name)
695
+ end
696
+ rescue NoMethodError, ArgumentError, TypeError, IndexError, KeyError => error
697
+ raise CodeModeExecutionError, "Invalid #{method_name} call: #{error.message}"
698
+ end
699
+
700
+ def invoke_enumerable(receiver, method_name, arguments, block)
701
+ enumerable = enumerable_value!(receiver, method_name)
702
+ case method_name
703
+ when "map", "collect" then require_block!(method_name, block).then { enumerable.map { |*items| tick!; block.call(*block_arguments(enumerable, items)) } }
704
+ when "select", "filter" then require_block!(method_name, block).then { enumerable.select { |*items| tick!; truthy?(block.call(*block_arguments(enumerable, items))) } }
705
+ when "reject" then require_block!(method_name, block).then { enumerable.reject { |*items| tick!; truthy?(block.call(*block_arguments(enumerable, items))) } }
706
+ when "each"
707
+ require_block!(method_name, block)
708
+ enumerable.each { |*items| tick!; block.call(*block_arguments(enumerable, items)) }
709
+ receiver
710
+ when "find" then require_block!(method_name, block).then { enumerable.find { |*items| tick!; truthy?(block.call(*block_arguments(enumerable, items))) } }
711
+ when "any?" then require_block!(method_name, block).then { enumerable.any? { |*items| tick!; truthy?(block.call(*block_arguments(enumerable, items))) } }
712
+ when "all?" then require_block!(method_name, block).then { enumerable.all? { |*items| tick!; truthy?(block.call(*block_arguments(enumerable, items))) } }
713
+ when "none?" then require_block!(method_name, block).then { enumerable.none? { |*items| tick!; truthy?(block.call(*block_arguments(enumerable, items))) } }
714
+ when "count"
715
+ block ? enumerable.count { |*items| tick!; truthy?(block.call(*block_arguments(enumerable, items))) } : enumerable.count(*arguments)
716
+ when "sort_by" then require_block!(method_name, block).then { enumerable.sort_by { |*items| tick!; block.call(*block_arguments(enumerable, items)) } }
717
+ when "reduce", "inject"
718
+ reduce_enumerable(enumerable, arguments, require_block!(method_name, block))
719
+ else invalid_receiver!(receiver, method_name)
720
+ end
721
+ rescue BreakSignal => signal
722
+ signal.value
723
+ end
724
+
725
+ def block_arguments(enumerable, items)
726
+ return items.first if enumerable.is_a?(Hash) && items.length == 1 && items.first.is_a?(Array)
727
+
728
+ items
729
+ end
730
+
731
+ def reduce_enumerable(enumerable, arguments, block)
732
+ values = enumerable.to_a
733
+ if arguments.empty?
734
+ return nil if values.empty?
735
+ accumulator = values.shift
736
+ else
737
+ require_arity!("reduce", arguments, 1)
738
+ accumulator = arguments.first
739
+ end
740
+ values.each { |item| tick!; accumulator = block.call(accumulator, item) }
741
+ accumulator
742
+ end
743
+
744
+ def safe_zero_argument_method(receiver, method_name)
745
+ allowed = case receiver
746
+ when Array then %w[reverse sort uniq compact flatten min max sum]
747
+ when Hash then %w[keys values]
748
+ when Range then %w[min max sum]
749
+ else []
750
+ end
751
+ invalid_receiver!(receiver, method_name) unless allowed.include?(method_name)
752
+ bounded_value!(receiver.public_send(method_name))
753
+ end
754
+
755
+ def safe_to_a(receiver)
756
+ return receiver.dup if receiver.is_a?(Array)
757
+ if receiver.is_a?(Range)
758
+ if receiver.begin.is_a?(Integer) && receiver.end.is_a?(Integer)
759
+ size = receiver.end - receiver.begin + (receiver.exclude_end? ? 0 : 1)
760
+ raise CodeModeLimitError, "Range contains too many values" if size > MAX_VALUE_ITEMS
761
+ end
762
+ return bounded_value!(receiver.to_a)
763
+ end
764
+ return receiver.to_a if receiver.is_a?(Hash)
765
+
766
+ invalid_receiver!(receiver, "to_a")
767
+ end
768
+
769
+ def safe_index(receiver, arguments)
770
+ raise CodeModeSyntaxError, "Indexing requires one or two arguments" unless (1..2).cover?(arguments.length)
771
+ raise CodeModeExecutionError, "#{receiver.class} values cannot be indexed" unless receiver.is_a?(Array) || receiver.is_a?(Hash) || receiver.is_a?(String)
772
+
773
+ receiver[*arguments]
774
+ rescue TypeError, IndexError => error
775
+ raise CodeModeExecutionError, "Invalid index: #{error.message}"
776
+ end
777
+
778
+ def enumerable_value!(receiver, method_name)
779
+ return receiver if receiver.is_a?(Array) || receiver.is_a?(Hash) || receiver.is_a?(Range)
780
+
781
+ invalid_receiver!(receiver, method_name)
782
+ end
783
+
784
+ def require_string!(value, method_name)
785
+ return value if value.is_a?(String)
786
+
787
+ invalid_receiver!(value, method_name)
788
+ end
789
+
790
+ def require_hash!(value, method_name)
791
+ return value if value.is_a?(Hash)
792
+
793
+ invalid_receiver!(value, method_name)
794
+ end
795
+
796
+ def require_array!(value, method_name)
797
+ return value if value.is_a?(Array)
798
+
799
+ invalid_receiver!(value, method_name)
800
+ end
801
+
802
+ def collection_value!(value, method_name)
803
+ return value if value.is_a?(Array) || value.is_a?(Hash) || value.is_a?(String) || value.is_a?(Range)
804
+
805
+ invalid_receiver!(value, method_name)
806
+ end
807
+
808
+ def indexable_value!(value, method_name)
809
+ return value if value.is_a?(Array) || value.is_a?(Hash) || value.is_a?(String)
810
+
811
+ invalid_receiver!(value, method_name)
812
+ end
813
+
814
+ def require_numeric!(value, method_name)
815
+ return value if value.is_a?(Numeric)
816
+
817
+ invalid_receiver!(value, method_name)
818
+ end
819
+
820
+ def safe_join(receiver, arguments)
821
+ separator = arguments.empty? ? "" : arguments.first.to_s
822
+ estimated = receiver.sum { |value| value.to_s.bytesize }
823
+ estimated += separator.bytesize * [receiver.length - 1, 0].max
824
+ raise CodeModeLimitError, "String result exceeds #{MAX_VALUE_BYTES} bytes" if estimated > MAX_VALUE_BYTES
825
+
826
+ receiver.join(*arguments)
827
+ end
828
+
829
+ def preflight_binary_size!(left, operator, right)
830
+ if operator == :+ && left.is_a?(String) && right.is_a?(String)
831
+ raise CodeModeLimitError, "String result exceeds #{MAX_VALUE_BYTES} bytes" if left.bytesize + right.bytesize > MAX_VALUE_BYTES
832
+ elsif operator == :+ && left.is_a?(Array) && right.is_a?(Array)
833
+ raise CodeModeLimitError, "Collection result exceeds #{MAX_VALUE_ITEMS} items" if left.length + right.length > MAX_VALUE_ITEMS
834
+ elsif operator == :* && right.is_a?(Integer) && (left.is_a?(String) || left.is_a?(Array))
835
+ size = left.is_a?(String) ? left.bytesize : left.length
836
+ limit = left.is_a?(String) ? MAX_VALUE_BYTES : MAX_VALUE_ITEMS
837
+ raise CodeModeLimitError, "Repeated value exceeds Code Mode limits" if right.positive? && size > limit / right
838
+ end
839
+ end
840
+
841
+ def bounded_value!(value)
842
+ if value.is_a?(String) && value.bytesize > MAX_VALUE_BYTES
843
+ raise CodeModeLimitError, "String result exceeds #{MAX_VALUE_BYTES} bytes"
844
+ end
845
+ if (value.is_a?(Array) || value.is_a?(Hash)) && value.length > MAX_VALUE_ITEMS
846
+ raise CodeModeLimitError, "Collection result exceeds #{MAX_VALUE_ITEMS} items"
847
+ end
848
+ value
849
+ end
850
+
851
+ def invalid_receiver!(receiver, method_name)
852
+ raise CodeModeExecutionError, "#{method_name} is not supported for #{receiver.class} values"
853
+ end
854
+
855
+ def require_block!(method_name, block)
856
+ raise CodeModeSyntaxError, "#{method_name} requires a block" unless block
857
+ block
858
+ end
859
+
860
+ def require_arity!(name, arguments, expected)
861
+ valid = expected.is_a?(Range) ? expected.cover?(arguments.length) : arguments.length == expected
862
+ return true if valid
863
+
864
+ raise CodeModeSyntaxError, "#{name} received #{arguments.length} arguments"
865
+ end
866
+
867
+ def tool_arguments(value)
868
+ return {} if value.nil?
869
+ raise CodeModeSyntaxError, "Tool arguments must be a hash" unless value.is_a?(Hash)
870
+
871
+ safe_tool_value(value)
872
+ end
873
+
874
+ def safe_tool_value(value, depth = 0, budget = { items: MAX_VALUE_ITEMS, bytes: MAX_VALUE_BYTES })
875
+ raise CodeModeLimitError, "Tool result nesting is too deep" if depth > 64
876
+ budget[:items] -= 1
877
+ raise CodeModeLimitError, "Tool result contains too many values" if budget[:items].negative?
878
+
879
+ case value
880
+ when String
881
+ budget[:bytes] -= value.bytesize
882
+ raise CodeModeLimitError, "Tool result contains too much string data" if budget[:bytes].negative?
883
+ value.dup
884
+ when nil, true, false, Numeric
885
+ value
886
+ when Array
887
+ value.map { |item| safe_tool_value(item, depth + 1, budget) }
888
+ when Hash
889
+ value.each_with_object({}) do |(key, item), result|
890
+ safe_key = key.is_a?(Symbol) ? key : key.to_s
891
+ result[safe_key] = safe_tool_value(item, depth + 1, budget)
892
+ end
893
+ else
894
+ raise CodeModeExecutionError, "Tool returned unsupported #{value.class} value"
895
+ end
896
+ end
897
+
898
+ def log(kind, values)
899
+ rendered = values.map { |value| kind == "p" ? value.inspect : value.to_s }.join(kind == "print" ? "" : " ")
900
+ rendered = "[WARN] #{rendered}" if kind == "warn"
901
+ bytes = rendered.bytesize
902
+ raise CodeModeLimitError, "Code Mode log limit exceeded" if @log_bytes + bytes > MAX_LOG_BYTES
903
+
904
+ @logs << rendered
905
+ @log_bytes += bytes
906
+ end
907
+
908
+ end
909
+ end
910
+
911
+ class CodeModeUtcpClient < Client
912
+ AGENT_PROMPT_TEMPLATE = <<~PROMPT.freeze
913
+ Use `codemode.search_tools` before writing a Code Mode program when tool names are unknown.
914
+ Call tools as `codemode.call_tool("manual.tool", key: value)`. Use the exact qualified name.
915
+ Code Mode accepts a constrained Ruby subset: local variables, JSON-like literals, arithmetic,
916
+ conditionals, bounded loops, and common Array/Hash/String transforms. The final expression or an
917
+ explicit `return` is the result. Use `puts`, `print`, `p`, or `warn` for captured logs. The values
918
+ `codemode.interfaces` and `codemode.get_tool_interface("manual.tool")` describe registered tools. Filesystem,
919
+ process, constants, imports, reflection, eval, and direct network access are unavailable.
920
+ PROMPT
921
+
922
+ def call_tool_chain(code, timeout_value = nil, timeout: nil, max_steps: CodeMode::DEFAULT_MAX_STEPS)
923
+ selected_timeout = timeout || timeout_value || CodeMode::DEFAULT_TIMEOUT
924
+ CodeMode.new(self).execute(code, timeout: selected_timeout, max_steps: max_steps)
925
+ end
926
+
927
+ def get_all_tools_ruby_interfaces
928
+ CodeMode.new(self).interfaces
929
+ end
930
+
931
+ def get_tool_interface(name)
932
+ CodeMode.new(self).tool_interface(name)
933
+ end
934
+ end
935
+
936
+ CodeModeClient = CodeModeUtcpClient
937
+ end
data/lib/utcp/errors.rb CHANGED
@@ -45,4 +45,18 @@ module UTCP
45
45
  end
46
46
  class SecurityError < Error; end
47
47
  class TimeoutError < ToolCallError; end
48
+
49
+ class CodeModeError < Error; end
50
+ class CodeModeSyntaxError < CodeModeError; end
51
+ class CodeModeTimeoutError < CodeModeError; end
52
+ class CodeModeLimitError < CodeModeError; end
53
+
54
+ class CodeModeExecutionError < CodeModeError
55
+ attr_reader :logs
56
+
57
+ def initialize(message, logs: [])
58
+ @logs = logs.dup.freeze
59
+ super(message)
60
+ end
61
+ end
48
62
  end
data/lib/utcp/version.rb CHANGED
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module UTCP
4
- VERSION = "1.1.0"
4
+ VERSION = "1.1.1"
5
5
  end
6
6
 
data/lib/utcp.rb CHANGED
@@ -28,6 +28,7 @@ require_relative "utcp/protocols/udp"
28
28
  require_relative "utcp/protocols/mcp"
29
29
  require_relative "utcp/protocols/webrtc"
30
30
  require_relative "utcp/client"
31
+ require_relative "utcp/code_mode"
31
32
 
32
33
  module UTCP
33
34
  register_auth("api_key", ApiKeyAuth)
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ruby-utcp
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.1.0
4
+ version: 1.1.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - ruby-utcp contributors
@@ -62,6 +62,7 @@ files:
62
62
  - README.md
63
63
  - examples/basic.rb
64
64
  - examples/cli.rb
65
+ - examples/code_mode.rb
65
66
  - examples/generated/__init__.py
66
67
  - examples/generated/utcp_pb2.py
67
68
  - examples/generated/utcp_pb2_grpc.py
@@ -93,6 +94,7 @@ files:
93
94
  - lib/ruby-utcp.rb
94
95
  - lib/utcp.rb
95
96
  - lib/utcp/client.rb
97
+ - lib/utcp/code_mode.rb
96
98
  - lib/utcp/config.rb
97
99
  - lib/utcp/errors.rb
98
100
  - lib/utcp/migration.rb