# 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" => "exit 42"}, %{}) IO.puts("Command: 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", "cwd" => "/tmp"}, %{}) IO.puts("Command: pwd (cwd: /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" => "yes | head -n 100"}, %{}) IO.puts("Command: 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 -> IO.puts(" [Mock executor called with: #{cmd}]") {:ok, %{"output" => "Mocked output for: #{cmd}", "exit_code" => 0}} end {:ok, _} = Tools.register(ShellTool, executor: mock_executor) {:ok, result} = Tools.invoke("shell", %{"command" => "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" => "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 (cwd) 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. """)