Add a Ruby UTCP coding-agent example with approvals and Code Mode - #1
Add a Ruby UTCP coding-agent example with approvals and Code Mode#1Raezil wants to merge 2 commits into
Conversation
There was a problem hiding this comment.
6 issues found and verified against the latest diff
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="examples/coding_agent/agent.rb">
<violation number="1" location="examples/coding_agent/agent.rb:58">
P2: When Code Mode is enabled and discovery contains a tool whose alias is `codemode_run_code`, this appends a duplicate function definition. `execute` routes that name to Code Mode, making the discovered UTCP tool unreachable; reject the reserved-name collision before appending.</violation>
</file>
<file name="examples/coding_agent.rb">
<violation number="1" location="examples/coding_agent.rb:71">
P2: Interactive tasks that hit the iteration limit or raise an error still make the CLI exit with status 0 because the loop discards `show_result`'s status and the rescued errors are not recorded. Preserve a nonzero session status and return it when the interactive loop ends so callers can detect incomplete work.</violation>
</file>
<file name="examples/coding_agent/utcp_workspace.rb">
<violation number="1" location="examples/coding_agent/utcp_workspace.rb:77">
P2: Code Mode can bypass the per-task tool budget through `codemode.call_tool_stream`, because inherited streaming dispatch never enters this override. Enforce the budget in shared dispatch or override `call_tool_streaming` as well.</violation>
</file>
<file name="examples/coding_agent/workspace.rb">
<violation number="1" location="examples/coding_agent/workspace.rb:138">
P2: When `old_text` has overlapping occurrences, this check treats them as unique and replaces only the first one, despite the tool promising exactly one unique literal block. Count overlapping matches before approving the edit.</violation>
</file>
<file name="examples/coding_agent/llm.rb">
<violation number="1" location="examples/coding_agent/llm.rb:38">
P1: HTTPS requests do not explicitly enable certificate verification, so a man-in-the-middle endpoint can receive the API key and workspace data. Set `http.verify_mode = OpenSSL::SSL::VERIFY_PEER` whenever SSL is enabled, matching the SDK HTTP transports.</violation>
</file>
<file name="test/coding_agent_cli_test.rb">
<violation number="1" location="test/coding_agent_cli_test.rb:15">
P3: The test name claims --help 'does not load the SDK', but the body only checks exit code 0 and two help flags. A regression that makes --help load ruby-utcp would pass this test silently. Add `refute defined?(UTCP)` before/after running --help (agent.rb and llm.rb don't require 'utcp', so it holds now), or rename the test to only claim it needs no API key.</violation>
</file>
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
| request["Accept"] = "application/json" | ||
| request.body = JSON.generate("model" => @model, "messages" => messages, "tools" => tools, "stream" => false) | ||
| http = Net::HTTP.new(@uri.host, @uri.port) | ||
| http.use_ssl = @uri.scheme == "https" |
There was a problem hiding this comment.
P1: HTTPS requests do not explicitly enable certificate verification, so a man-in-the-middle endpoint can receive the API key and workspace data. Set http.verify_mode = OpenSSL::SSL::VERIFY_PEER whenever SSL is enabled, matching the SDK HTTP transports.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At examples/coding_agent/llm.rb, line 38:
<comment>HTTPS requests do not explicitly enable certificate verification, so a man-in-the-middle endpoint can receive the API key and workspace data. Set `http.verify_mode = OpenSSL::SSL::VERIFY_PEER` whenever SSL is enabled, matching the SDK HTTP transports.</comment>
<file context>
@@ -0,0 +1,74 @@
+ request["Accept"] = "application/json"
+ request.body = JSON.generate("model" => @model, "messages" => messages, "tools" => tools, "stream" => false)
+ http = Net::HTTP.new(@uri.host, @uri.port)
+ http.use_ssl = @uri.scheme == "https"
+ http.open_timeout = 15
+ http.read_timeout = 120
</file context>
| http.use_ssl = @uri.scheme == "https" | |
| http.use_ssl = @uri.scheme == "https" | |
| http.verify_mode = OpenSSL::SSL::VERIFY_PEER if http.use_ssl? |
| function(alias_name, tool.description, tool.inputs.to_h) | ||
| end | ||
| if @code_mode | ||
| @tools << function("codemode_run_code", "Compose multiple workspace tools in restricted Ruby; approvals still apply.", |
There was a problem hiding this comment.
P2: When Code Mode is enabled and discovery contains a tool whose alias is codemode_run_code, this appends a duplicate function definition. execute routes that name to Code Mode, making the discovered UTCP tool unreachable; reject the reserved-name collision before appending.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At examples/coding_agent/agent.rb, line 58:
<comment>When Code Mode is enabled and discovery contains a tool whose alias is `codemode_run_code`, this appends a duplicate function definition. `execute` routes that name to Code Mode, making the discovered UTCP tool unreachable; reject the reserved-name collision before appending.</comment>
<file context>
@@ -0,0 +1,166 @@
+ function(alias_name, tool.description, tool.inputs.to_h)
+ end
+ if @code_mode
+ @tools << function("codemode_run_code", "Compose multiple workspace tools in restricted Ruby; approvals still apply.",
+ "type" => "object", "properties" => { "code" => { "type" => "string" } },
+ "required" => ["code"], "additionalProperties" => false)
</file context>
| next | ||
| end | ||
| begin | ||
| show_result(agent.run(line.strip)) |
There was a problem hiding this comment.
P2: Interactive tasks that hit the iteration limit or raise an error still make the CLI exit with status 0 because the loop discards show_result's status and the rescued errors are not recorded. Preserve a nonzero session status and return it when the interactive loop ends so callers can detect incomplete work.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At examples/coding_agent.rb, line 71:
<comment>Interactive tasks that hit the iteration limit or raise an error still make the CLI exit with status 0 because the loop discards `show_result`'s status and the rescued errors are not recorded. Preserve a nonzero session status and return it when the interactive loop ends so callers can detect incomplete work.</comment>
<file context>
@@ -0,0 +1,123 @@
+ next
+ end
+ begin
+ show_result(agent.run(line.strip))
+ rescue StandardError => error
+ @error.puts("Error: #{safe_text(error.message)}")
</file context>
| @remaining_calls = limit | ||
| end | ||
|
|
||
| def call_tool(name, arguments = {}) |
There was a problem hiding this comment.
P2: Code Mode can bypass the per-task tool budget through codemode.call_tool_stream, because inherited streaming dispatch never enters this override. Enforce the budget in shared dispatch or override call_tool_streaming as well.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At examples/coding_agent/utcp_workspace.rb, line 77:
<comment>Code Mode can bypass the per-task tool budget through `codemode.call_tool_stream`, because inherited streaming dispatch never enters this override. Enforce the budget in shared dispatch or override `call_tool_streaming` as well.</comment>
<file context>
@@ -0,0 +1,84 @@
+ @remaining_calls = limit
+ end
+
+ def call_tool(name, arguments = {})
+ raise UTCP::ToolCallError, "workspace tool-call budget exhausted" unless @remaining_calls.positive?
+
</file context>
| string!(new_text, "new_text") | ||
| original = read_text(resolve(path)) | ||
| check_revision!(original, expected_sha256) | ||
| unless original.scan(Regexp.new(Regexp.escape(old_text))).length == 1 |
There was a problem hiding this comment.
P2: When old_text has overlapping occurrences, this check treats them as unique and replaces only the first one, despite the tool promising exactly one unique literal block. Count overlapping matches before approving the edit.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At examples/coding_agent/workspace.rb, line 138:
<comment>When `old_text` has overlapping occurrences, this check treats them as unique and replaces only the first one, despite the tool promising exactly one unique literal block. Count overlapping matches before approving the edit.</comment>
<file context>
@@ -0,0 +1,277 @@
+ string!(new_text, "new_text")
+ original = read_text(resolve(path))
+ check_revision!(original, expected_sha256)
+ unless original.scan(Regexp.new(Regexp.escape(old_text))).length == 1
+ raise ArgumentError, "old_text must match exactly once; read the file and choose a unique block"
+ end
</file context>
| def test_help_does_not_need_api_keys_or_load_the_sdk | ||
| assert_equal 0, @cli.run(["--help"]) | ||
| assert_includes @out.string, "--workspace" | ||
| assert_includes @out.string, "--codemode" | ||
| end | ||
|
|
There was a problem hiding this comment.
P3: The test name claims --help 'does not load the SDK', but the body only checks exit code 0 and two help flags. A regression that makes --help load ruby-utcp would pass this test silently. Add refute defined?(UTCP) before/after running --help (agent.rb and llm.rb don't require 'utcp', so it holds now), or rename the test to only claim it needs no API key.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At test/coding_agent_cli_test.rb, line 15:
<comment>The test name claims --help 'does not load the SDK', but the body only checks exit code 0 and two help flags. A regression that makes --help load ruby-utcp would pass this test silently. Add `refute defined?(UTCP)` before/after running --help (agent.rb and llm.rb don't require 'utcp', so it holds now), or rename the test to only claim it needs no API key.</comment>
<file context>
@@ -0,0 +1,30 @@
+ @cli = RubyUTCPAgent::CLI.new(input: @input, output: @out, error: @err, env: {})
+ end
+
+ def test_help_does_not_need_api_keys_or_load_the_sdk
+ assert_equal 0, @cli.run(["--help"])
+ assert_includes @out.string, "--workspace"
</file context>
| def test_help_does_not_need_api_keys_or_load_the_sdk | |
| assert_equal 0, @cli.run(["--help"]) | |
| assert_includes @out.string, "--workspace" | |
| assert_includes @out.string, "--codemode" | |
| end | |
| def test_help_does_not_need_api_keys_or_load_the_sdk | |
| refute defined?(UTCP), "SDK must not be loaded for --help" | |
| assert_equal 0, @cli.run(["--help"]) | |
| assert_includes @out.string, "--workspace" | |
| assert_includes @out.string, "--codemode" | |
| refute defined?(UTCP), "--help must not load the SDK" | |
| end |
Summary
Adds
examples/coding_agent.rb, a terminal coding agent with one-shot and interactive modes, OpenRouter-compatible chat completions, six workspace tools, and optional UTCP Code Mode.coding_agent_localprotocol. Existing SDK transports are unchanged.examples/README.md.Run
Use
--prompt 'task'for one-shot mode and--read-onlyto prohibit edits and commands.Verification
Locally verified on Ruby 3.3.8: 26 tests, 100 assertions, 0 failures, 0 errors, 0 skips, plus Ruby syntax checks and CLI help. HTTP tests use a local TCP server; provider/client doubles are explicit in agent-loop unit tests.
A separate
test/coding_agent_utcp_test.rbuses the real SDK in a subprocess to verify discovery, invocation, Code Mode edits, approval enforcement, client isolation and call budgets. It is included in the existingrake testdiscovery. The complete SDK cannot be installed in the local execution environment, so real-SDK/full-suite verification was performed by repository CI instead.Latest commit
1394950: all eight CI test/gem-build jobs passed on Ruby 2.6, 2.7, 3.0, 3.1, 3.2, 3.3, 3.4, and 4.0. These jobs run the complete unit/regression suite, including the real-SDK integration test. The coverage-and-transports job also passed. The transport-soak and native-transports jobs were still running at the last check; no claim of a fully completed workflow is made.CI run: https://github.com/universal-tool-calling-protocol/ruby-utcp/actions/runs/34507245625
The first run exposed an older-Ruby test-loader error (
require_relativeinsideruby -e). Commit1394950fixed it using an explicit absolute require path; the corrected Ruby 2.6 and 2.7 jobs are green.No paid/live LLM call was performed.
Security scope
These are application guardrails, not an OS sandbox. Approved commands can execute arbitrary code with the user's permissions.
--yesexplicitly auto-approves both edits and commands; read-only mode still wins. Source/tool output is sent to the configured LLM provider. Code Mode batches are not transactions, and completed edits are not automatically rolled back. The example does not automatically commit or push workspace changes.