Skip to content

Add a Ruby UTCP coding-agent example with approvals and Code Mode - #1

Closed
Raezil wants to merge 2 commits into
mainfrom
examples/ruby-utcp-coding-agent
Closed

Add a Ruby UTCP coding-agent example with approvals and Code Mode#1
Raezil wants to merge 2 commits into
mainfrom
examples/ruby-utcp-coding-agent

Conversation

@Raezil

@Raezil Raezil commented Sep 10, 2026

Copy link
Copy Markdown
Collaborator

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.

  • Tool discovery, schemas, namespacing and invocation use the actual UTCP client through an example-local, in-process coding_agent_local protocol. Existing SDK transports are unchanged.
  • File edits and every command require approval by default. Read-only mode, SHA-256 revision checks, bounded output, command process-group timeouts, and per-task tool budgets are enforced below the model layer.
  • The agent preserves tool-call IDs and opaque provider reasoning metadata, handles malformed arguments/tool errors, and reports iteration exhaustion without claiming success.
  • Adds a usage/security guide and an entry in examples/README.md.

Run

export OPENROUTER_API_KEY='your-key'
export OPENROUTER_MODEL='your-tool-capable-model-id'
bundle exec ruby -Ilib examples/coding_agent.rb --workspace /path/to/project --codemode

Use --prompt 'task' for one-shot mode and --read-only to 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.rb uses 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 existing rake test discovery. 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_relative inside ruby -e). Commit 1394950 fixed 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. --yes explicitly 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.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>
Suggested change
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.",

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>

Comment thread examples/coding_agent.rb
next
end
begin
show_result(agent.run(line.strip))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 = {})

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>

Comment on lines +15 to +20
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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>
Suggested change
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

@Raezil Raezil closed this Sep 10, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant