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 +4 -4
- data/CHANGELOG.md +14 -1
- data/README.md +7 -3
- data/lib/utcp/client.rb +16 -7
- data/lib/utcp/code_mode/runtime.rb +1 -1
- data/lib/utcp/code_mode/value_limits.rb +11 -1
- data/lib/utcp/code_mode.rb +1 -1
- data/lib/utcp/protocols/websocket.rb +11 -9
- data/lib/utcp/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 265426eb4dd2ca952c81a23b71aeb8c1f0b1c90f8fbf247ec502279b0129ffba
|
|
4
|
+
data.tar.gz: 79c0d4d21648777686b7caf3cc32f333cb7001bfcf8fa3f10c340195a706f449
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
data/lib/utcp/code_mode.rb
CHANGED
|
@@ -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(
|
|
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
|
|
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(
|
|
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