Current section

Files

Jump to
codex_sdk examples shell_tool.exs
Raw

examples/shell_tool.exs

# Example: Shell Tool Usage
# Run: mix run examples/shell_tool.exs
#
# Demonstrates the fully-featured Shell hosted tool with:
# - Default executor using erlexec
# - Custom executor for testing/mocking
# - Approval integration
# - Timeout handling
# - Output truncation
Mix.Task.run("app.start")
alias Codex.Tools
alias Codex.Tools.ShellTool
IO.puts("""
=== Shell Tool Example ===
This example demonstrates the Shell hosted tool capabilities.
""")
# Reset tools for clean state
Tools.reset!()
# -----------------------------------------------------------------------------
# 1. Basic shell execution with default executor
# -----------------------------------------------------------------------------
IO.puts("\n1. Basic shell execution (default executor)")
IO.puts("-" |> String.duplicate(50))
{:ok, _handle} = Tools.register(ShellTool)
{:ok, result} = Tools.invoke("shell", %{"command" => ["echo", "Hello from shell!"]}, %{})
IO.puts("Command: echo Hello from shell!")
IO.puts("Output: #{String.trim(result["output"])}")
IO.puts("Exit code: #{result["exit_code"]}")
IO.puts("Success: #{result["success"]}")
Tools.reset!()
# -----------------------------------------------------------------------------
# 2. Capturing exit codes
# -----------------------------------------------------------------------------
IO.puts("\n2. Capturing non-zero exit codes")
IO.puts("-" |> String.duplicate(50))
{:ok, _} = Tools.register(ShellTool)
{:ok, result} = Tools.invoke("shell", %{"command" => ["sh", "-c", "exit 42"]}, %{})
IO.puts("Command: sh -c \"exit 42\"")
IO.puts("Exit code: #{result["exit_code"]}")
IO.puts("Success: #{result["success"]}")
Tools.reset!()
# -----------------------------------------------------------------------------
# 3. Working directory support
# -----------------------------------------------------------------------------
IO.puts("\n3. Working directory support")
IO.puts("-" |> String.duplicate(50))
{:ok, _} = Tools.register(ShellTool)
{:ok, result} = Tools.invoke("shell", %{"command" => ["pwd"], "workdir" => "/tmp"}, %{})
IO.puts("Command: pwd (workdir: /tmp)")
IO.puts("Output: #{String.trim(result["output"])}")
Tools.reset!()
# -----------------------------------------------------------------------------
# 4. Output truncation
# -----------------------------------------------------------------------------
IO.puts("\n4. Output truncation")
IO.puts("-" |> String.duplicate(50))
{:ok, _} = Tools.register(ShellTool, max_output_bytes: 50)
{:ok, result} = Tools.invoke("shell", %{"command" => ["sh", "-c", "yes | head -n 100"]}, %{})
IO.puts("Command: sh -c \"yes | head -n 100\" (max 50 bytes)")
IO.puts("Output length: #{byte_size(result["output"])} bytes")
IO.puts("Truncated: #{String.ends_with?(result["output"], "... (truncated)")}")
Tools.reset!()
# -----------------------------------------------------------------------------
# 5. Approval callback integration
# -----------------------------------------------------------------------------
IO.puts("\n5. Approval callback integration")
IO.puts("-" |> String.duplicate(50))
approval = fn cmd, _ctx ->
if String.contains?(cmd, "rm") do
{:deny, "rm commands are not allowed"}
else
:ok
end
end
{:ok, _} = Tools.register(ShellTool, approval: approval)
# Safe command - should succeed
{:ok, result} = Tools.invoke("shell", %{"command" => ["echo", "safe"]}, %{})
IO.puts("Command: echo safe")
IO.puts("Result: #{String.trim(result["output"])}")
# Dangerous command - should be denied
case Tools.invoke("shell", %{"command" => ["rm", "/some/file"]}, %{}) do
{:error, {:approval_denied, reason}} ->
IO.puts("\nCommand: rm /some/file")
IO.puts("Denied: #{inspect(reason)}")
{:ok, _} ->
IO.puts("Unexpected: command was allowed")
end
Tools.reset!()
# -----------------------------------------------------------------------------
# 6. Custom executor for testing
# -----------------------------------------------------------------------------
IO.puts("\n6. Custom executor for testing/mocking")
IO.puts("-" |> String.duplicate(50))
mock_executor = fn %{"command" => cmd}, _ctx, _meta ->
formatted = if is_list(cmd), do: Enum.join(cmd, " "), else: cmd
IO.puts(" [Mock executor called with: #{formatted}]")
{:ok, %{"output" => "Mocked output for: #{formatted}", "exit_code" => 0}}
end
{:ok, _} = Tools.register(ShellTool, executor: mock_executor)
{:ok, result} = Tools.invoke("shell", %{"command" => ["echo", "any-command"]}, %{})
IO.puts("Output: #{result["output"]}")
Tools.reset!()
# -----------------------------------------------------------------------------
# 7. Timeout handling
# -----------------------------------------------------------------------------
IO.puts("\n7. Timeout handling")
IO.puts("-" |> String.duplicate(50))
{:ok, _} = Tools.register(ShellTool, timeout_ms: 100)
case Tools.invoke("shell", %{"command" => ["sleep", "5"]}, %{}) do
{:error, :timeout} ->
IO.puts("Command: sleep 5 (timeout: 100ms)")
IO.puts("Result: Timed out as expected!")
{:ok, _} ->
IO.puts("Unexpected: command completed (should have timed out)")
end
Tools.reset!()
# -----------------------------------------------------------------------------
# 8. Combining options
# -----------------------------------------------------------------------------
IO.puts("\n8. Combining multiple options")
IO.puts("-" |> String.duplicate(50))
approval = fn cmd, ctx ->
IO.puts(" [Approval check for: #{cmd}, user: #{ctx[:user] || "unknown"}]")
:ok
end
{:ok, _} =
Tools.register(ShellTool,
approval: approval,
timeout_ms: 5000,
max_output_bytes: 100,
cwd: "/tmp"
)
{:ok, result} =
Tools.invoke(
"shell",
%{"command" => ["echo", "Combined options test"]},
%{user: "admin"}
)
IO.puts("Output: #{String.trim(result["output"])}")
IO.puts("Success: #{result["success"]}")
Tools.reset!()
# -----------------------------------------------------------------------------
# 9. Direct invocation without registry
# -----------------------------------------------------------------------------
IO.puts("\n9. Direct invocation (without registry)")
IO.puts("-" |> String.duplicate(50))
executor = fn _args, _ctx, _meta ->
{:ok, %{"output" => "Direct call works!", "exit_code" => 0}}
end
context = %{metadata: %{executor: executor}}
args = %{"command" => ["echo", "test"]}
{:ok, result} = ShellTool.invoke(args, context)
IO.puts("Output: #{result["output"]}")
# -----------------------------------------------------------------------------
# Summary
# -----------------------------------------------------------------------------
IO.puts("\n" <> String.duplicate("=", 50))
IO.puts("Shell Tool Example Complete!")
IO.puts("""
Features demonstrated:
- Basic command execution with default erlexec executor
- Exit code capture and success flag
- Working directory (workdir) support
- Output truncation for large outputs
- Approval callback integration (allow/deny)
- Custom executor for testing/mocking
- Timeout handling for long-running commands
- Combined options usage
- Direct invocation without registry
See lib/codex/tools/shell_tool.ex for full documentation.
""")