ruby-utcp 1.1.3 → 1.1.4

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: 1869c15823d927455e8339580df8200a4eae60a1313a1384b2d98f793786e78d
4
- data.tar.gz: 3f63ab346e1316dd5ba26e367637f3a1c39d8b7202d1b5c117c3885cf0c8405a
3
+ metadata.gz: 265426eb4dd2ca952c81a23b71aeb8c1f0b1c90f8fbf247ec502279b0129ffba
4
+ data.tar.gz: 79c0d4d21648777686b7caf3cc32f333cb7001bfcf8fa3f10c340195a706f449
5
5
  SHA512:
6
- metadata.gz: 0bb88097ddb4f9170b8aeec82fb14d34fc708abc15ac7cfed54df9c03737105183c0fd09ca202c2ae53fbd276e85221393c4d8adfa36f80f5e05296b1c4ca860
7
- data.tar.gz: 4c9c0147d82493982d2717bc56b4f5b0486b09fa1c37b4a2a5d4ebf7b04dd0859b28ef4361eb6d7e8458d27b6a1975073b9af68784401695a300f97f2fa7ecfd
6
+ metadata.gz: 698ae3e9f0600fedeaa7adf2ae51c4e6872affed4a2da34454456c51f444a25d83ae59d6984d395b77f85ca36fa426731bcca6565cd0e7b1fc915473cb1caf3a
7
+ data.tar.gz: 1a0ff70255dbd8b4ab2b7b323e638d726646423833144eed365d3fd9236908a038e0652c18548e9095ecbbfa87a87b2d01b5167dc8d93125918b09054326f21d
data/CHANGELOG.md CHANGED
@@ -2,12 +2,21 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ - Raise the Code Mode value budget to 30 MiB, account for string/symbol hash keys, and enforce shared byte, item, and step limits while collecting streams.
6
+ - Close temporary discovery sessions when inspecting required variables, preserving active MCP, WebRTC, and WebSocket sessions. Isolate persistent WebSocket connections between clients and close transient discovery sockets on failure.
7
+ - Run real gRPC/WebRTC tests in CI using pinned, unmodified native dependencies and libdatachannel 0.24.5. Keep the additional backend shutdown probes opt-in rather than requiring modified WebRTC bindings.
8
+ - Correct the README coverage thresholds and document the native integration setup.
9
+
10
+ ## 1.1.3
11
+
5
12
  - Split Code Mode into focused components while preserving the public API; fix `do...end` block results.
6
13
  - Add branch-focused interpreter tests, concurrent request tests, and local HTTP/TCP/WebSocket fault injection.
7
14
  - Isolate WebRTC peers between clients, close failed registrations, and align native offer/answer negotiation with `webrtc-ruby`.
8
15
  - Close WebSocket sockets when the opening handshake fails.
9
16
  - Add an opt-in real gRPC/WebRTC integration suite and raise coverage gates to 80% lines / 60% branches.
10
- - Bound native integration runs with a subprocess watchdog. Include an opt-in source patch for `webrtc-ruby` 1.0.0 shutdown deadlocks, pointer-lifetime protection, and real callback/concurrent-shutdown regression tests.
17
+ - Bound native integration runs with a subprocess watchdog and add real callback/concurrent-shutdown regression tests for the locally patched WebRTC backend.
18
+
19
+ ## 1.1.2
11
20
 
12
21
  - Isolate MCP sessions and resource mappings between clients and clean up failed registrations.
13
22
  - Follow MCP tool/resource pagination and raise `ToolCallError` for tool failures, preserving the original payload.
@@ -16,6 +25,10 @@
16
25
  - Declare `base64` and `logger` runtime dependencies for Bundler compatibility.
17
26
  - Add regression tests, coverage checks, a Ruby version CI matrix, and local transport integration checks.
18
27
 
28
+ ## 1.1.1
29
+
30
+ - Add Code Mode for composing tools with a constrained Ruby interpreter, progressive tool discovery, captured logs, and execution limits.
31
+
19
32
  ## 1.1.0
20
33
 
21
34
  - Initial Ruby implementation of the UTCP 1.1 client and data model.
data/README.md CHANGED
@@ -267,7 +267,7 @@ load_variables_from:
267
267
  env_file_path: .env
268
268
  ```
269
269
 
270
- Manual-specific names are tried first. For a manual named `weather_api`, `${TOKEN}` first looks for `weather__api_TOKEN`, then `TOKEN`. Use `get_required_variables_for_manual_and_tools` or `get_required_variables_for_registered_tool` to inspect requirements without exposing values.
270
+ Manual-specific names are tried first. For a manual named `weather_api`, `${TOKEN}` first looks for `weather__api_TOKEN`, then `TOKEN`. Use `get_required_variables_for_manual_and_tools` or `get_required_variables_for_registered_tool` to inspect requirements without exposing values. Discovery performed by the former uses a temporary client and closes its sessions before returning, including on failure; existing client sessions stay open.
271
271
 
272
272
  ## Migrating v0.1 documents
273
273
 
@@ -315,7 +315,7 @@ Use `codemode.call_tool_stream` to collect a streaming call into an array. `code
315
315
 
316
316
  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.
317
317
 
318
- The 1 MiB value budget includes numeric payloads as well as strings. Integer powers and products are checked before allocating their results, and non-finite floating-point results are rejected. Power checks use a conservative size estimate and can reject expressions close to the limit.
318
+ The 30 MiB (31,457,280 bytes) value budget is shared across strings, numeric payloads, and hash keys within each tool argument, result, or collected stream. String and symbol keys count toward both the byte budget and the 10,000-value limit. Streams are checked one item at a time and stop as soon as the shared budget is exceeded. Integer powers and products are checked before allocating their results, and non-finite floating-point results are rejected. Power checks use a conservative size estimate and can reject expressions close to the limit. Source code is limited to 64 KiB and captured logs to 1 MiB per execution.
319
319
 
320
320
  ## Custom protocol plugins
321
321
 
@@ -361,4 +361,8 @@ bundle exec make standard-demo
361
361
  gem build ruby-utcp.gemspec
362
362
  ```
363
363
 
364
- CI runs the tests and gem build on Ruby 2.6, 2.7, 3.0–3.4, and 4.0. A separate Ruby 3.4 job runs the local transport examples and enforces minimum coverage of 75% of executable lines and 45% of branches. The standard demo uses WEBrick, included as a development dependency; native gRPC/WebRTC backends remain optional.
364
+ CI runs the tests and gem build on Ruby 2.6, 2.7, 3.0–3.4, and 4.0. A separate Ruby 3.4 job runs the local transport examples and enforces minimum coverage of 80% of executable lines and 60% of branches. The standard demo uses WEBrick, included as a development dependency.
365
+
366
+ A dedicated Ruby 3.4 CI job sets `UTCP_NATIVE_TESTS=1` to install the original, unmodified backends from the Gemfile, builds libdatachannel 0.24.5 and the WebRTC extension, and runs `bundle exec rake native`. The test subprocess has a 45-second watchdog. The native dependencies remain optional for applications using the gem.
367
+
368
+ The default native suite checks discovery, real calls, streaming, concurrent requests, timeouts, and client isolation. Additional backend shutdown probes are opt-in with `UTCP_WEBRTC_SHUTDOWN_REGRESSIONS=1`; they require callback shutdown guarantees that the stock `webrtc-ruby` 1.0.0 release does not provide and are not part of the CI job. No WebRTC patches are installed or applied.
data/lib/utcp/client.rb CHANGED
@@ -132,12 +132,22 @@ module UTCP
132
132
  return required unless required.empty?
133
133
 
134
134
  substituted = substitute_template(template, template.name)
135
- result = fetch_protocol(substituted.call_template_type).register_manual(self, substituted)
136
- return [] unless result.success?
137
-
138
- result.manual.tools.flat_map do |tool|
139
- variable_substitutor.find_required_variables(tool.tool_call_template.to_h, template.name)
140
- end.uniq
135
+ protocol = fetch_protocol(substituted.call_template_type)
136
+ # Give discovery its own session owner and repository, even for an existing manual.
137
+ inspection_config = config.dup
138
+ inspection_config.tool_repository = InMemoryToolRepository.new
139
+ inspection_config.manual_call_templates = []
140
+ inspection_client = Client.new(root_dir: root_dir, config: inspection_config, logger: logger)
141
+ begin
142
+ result = protocol.register_manual(inspection_client, substituted)
143
+ return [] unless result.success?
144
+
145
+ result.manual.tools.flat_map do |tool|
146
+ variable_substitutor.find_required_variables(tool.tool_call_template.to_h, template.name)
147
+ end.uniq
148
+ ensure
149
+ protocol.deregister_manual(inspection_client, substituted)
150
+ end
141
151
  end
142
152
 
143
153
  def get_required_variables_for_registered_tool(tool_name)
@@ -214,4 +224,3 @@ module UTCP
214
224
  end
215
225
  UtcpClient = Client
216
226
  end
217
-
@@ -14,7 +14,7 @@ module UTCP
14
14
  safe_tool_value(@client.call_tool(arguments[0], tool_arguments(arguments[1])))
15
15
  when "call_tool_stream", "call_tool_streaming"
16
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)
17
+ safe_tool_stream(@client.call_tool_streaming(arguments[0], tool_arguments(arguments[1])))
18
18
  when "search_tools"
19
19
  require_arity!("codemode.search_tools", arguments, 1..2)
20
20
  options = arguments[1].is_a?(Hash) ? arguments[1] : {}
@@ -81,13 +81,23 @@ module UTCP
81
81
  value.map { |item| safe_tool_value(item, depth + 1, budget) }
82
82
  when Hash
83
83
  value.each_with_object({}) do |(key, item), result|
84
- safe_key = key.is_a?(Symbol) ? key : key.to_s
84
+ key_string = safe_tool_value(key.to_s, depth + 1, budget)
85
+ safe_key = key.is_a?(Symbol) ? key : key_string
85
86
  result[safe_key] = safe_tool_value(item, depth + 1, budget)
86
87
  end
87
88
  else
88
89
  raise CodeModeExecutionError, "Tool returned unsupported #{value.class} value"
89
90
  end
90
91
  end
92
+
93
+ def safe_tool_stream(stream)
94
+ # Reserve the root array so streamed and ordinary results use the same budget.
95
+ budget = { items: MAX_VALUE_ITEMS - 1, bytes: MAX_VALUE_BYTES }
96
+ stream.each_with_object([]) do |item, result|
97
+ tick!
98
+ result << safe_tool_value(item, 1, budget)
99
+ end
100
+ end
91
101
  end
92
102
  end
93
103
  end
@@ -12,7 +12,7 @@ module UTCP
12
12
  DEFAULT_MAX_STEPS = 100_000
13
13
  MAX_CODE_BYTES = 64 * 1024
14
14
  MAX_LOG_BYTES = 1024 * 1024
15
- MAX_VALUE_BYTES = 1024 * 1024
15
+ MAX_VALUE_BYTES = 30 * 1024 * 1024
16
16
  MAX_VALUE_ITEMS = 10_000
17
17
  MAX_INTEGER_BITS = MAX_VALUE_BYTES * 8
18
18
 
@@ -250,33 +250,35 @@ module UTCP
250
250
 
251
251
  def register_manual(client, template)
252
252
  assert_websocket_template!(template)
253
- entry, transient = connection_for(template, {})
253
+ entry, transient = connection_for(client, template, {})
254
254
  payload = entry.mutex.synchronize do
255
255
  entry.connection.send_text(JSON.generate("type" => "utcp"))
256
256
  _opcode, bytes = entry.connection.read_message
257
257
  bytes
258
258
  end
259
- entry.connection.close if transient
260
259
  success(template, manual_from_payload(template, payload, source: "WebSocket discovery response"))
261
260
  rescue StandardError => error
262
261
  client.logger.warn("Unable to register WebSocket manual #{template.name.inspect}: #{error.message}")
263
262
  failure(template, error)
263
+ ensure
264
+ entry.connection.close if entry && transient
264
265
  end
265
266
 
266
- def deregister_manual(_client, template)
267
- prefix = [template.url, template.protocol].join("\0") + "\0"
267
+ def deregister_manual(client, template)
268
268
  entries = @connections_mutex.synchronize do
269
- keys = @connections.keys.select { |key| key.start_with?(prefix) }
269
+ keys = @connections.keys.select do |owner, url, subprotocol, _key|
270
+ owner.equal?(client) && url == template.url && subprotocol == template.protocol
271
+ end
270
272
  keys.map { |key| @connections.delete(key) }
271
273
  end
272
274
  entries.each { |entry| entry.connection.close }
273
275
  nil
274
276
  end
275
277
 
276
- def call_tool(_client, tool_name, tool_args, template)
278
+ def call_tool(client, tool_name, tool_args, template)
277
279
  assert_websocket_template!(template)
278
280
  args = Utils.stringify_keys(tool_args || {})
279
- entry, transient, message_args = connection_for(template, args, include_arguments: true)
281
+ entry, transient, message_args = connection_for(client, template, args, include_arguments: true)
280
282
  result = entry.mutex.synchronize do
281
283
  message = format_message(template, message_args)
282
284
  entry.connection.send_text(message)
@@ -302,7 +304,7 @@ module UTCP
302
304
  raise ValidationError, "WebSocket protocol requires a WebSocketCallTemplate"
303
305
  end
304
306
 
305
- def connection_for(template, arguments, include_arguments: false)
307
+ def connection_for(client, template, arguments, include_arguments: false)
306
308
  args = arguments.dup
307
309
  headers = Utils.stringify_keys(template.headers || {})
308
310
  template.header_fields.each { |field| headers[field] = args.delete(field).to_s if args.key?(field) }
@@ -315,7 +317,7 @@ module UTCP
315
317
  headers["Cookie"] = cookies.map { |key, value| "#{key}=#{value}" }.join("; ") unless cookies.empty?
316
318
  url = append_query(template.url, query)
317
319
  WebSocketURLSecurity.validate!(url)
318
- key = connection_key(template, url, headers)
320
+ key = [client, template.url, template.protocol, connection_key(template, url, headers)]
319
321
  if template.keep_alive
320
322
  entry = @connections_mutex.synchronize do
321
323
  current = @connections[key]
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.3"
4
+ VERSION = "1.1.4"
5
5
  end
6
6
 
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.3
4
+ version: 1.1.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - ruby-utcp contributors