SPB Git

Repository Public

tools-local-shell.md 28.0 KB 873 lines markdown

1# Local shell23> For the complete documentation index, see [llms.txt](/llms.txt). Markdown versions of documentation pages are available by appending `.md` to the page URL.45The local shell tool is outdated. For new use cases, use the6  [`shell`](https://developers.openai.com/api/docs/guides/tools-shell) tool with GPT-5.1 instead. [Learn7  more](https://developers.openai.com/api/docs/guides/tools-shell).89Local shell is a tool that allows agents to run shell commands locally on a machine you or the user provides. It's designed to work with [Codex CLI](https://github.com/openai/codex) and [`codex-mini-latest`](https://developers.openai.com/api/docs/models/codex-mini-latest). Commands are executed inside your own runtime, so **you are fully in control of which commands actually run**. The API only returns instructions; it does not execute them on OpenAI infrastructure.1011Local shell is available through the [Responses API](https://developers.openai.com/api/docs/guides/migrate-to-responses) for use with [`codex-mini-latest`](https://developers.openai.com/api/docs/models/codex-mini-latest). It is not available on other models or via the Chat Completions API.1213Running arbitrary shell commands can be dangerous. Always sandbox execution14or add strict allowlists or deny lists before forwarding a command to the system15shell.16171819See [Codex CLI](https://github.com/openai/codex) for reference implementation.2021## How it works2223The local shell tool enables agents to run in a continuous loop with access to a terminal.2425The model sends shell commands, which your code executes on a local machine before returning the output to the model. This loop allows the model to complete the build-test-run loop without additional user intervention.2627Your code must implement a loop that listens for `local_shell_call` output items and executes the commands they contain. We strongly recommend sandboxing execution to prevent unexpected commands from running.28293031Integrating the local shell tool32333435These are the high-level steps you need to follow to integrate the local shell tool in your application:36371. **Send a request to the model**:38   Include the `local_shell` tool as part of the available tools.39402. **Receive a response from the model**:41   Check if the response has any `local_shell_call` items.42   This tool call contains an action like `exec` with a command to execute.43443. **Execute the requested action**:45   Run the command in the local environment you control.46474. **Return the action output**:48   After executing the action, return the command output to the model.49505. **Repeat**:51   Send a new request with the updated state as a `local_shell_call_output`, and repeat this loop until the model stops requesting actions or you decide to stop.5253## Example workflow5455Below is a minimal example showing the request/response loop. Choose a language56to see the equivalent workflow for its SDK. For brevity, production-grade57sandboxing and security checks are omitted—**do not execute untrusted commands58in production without additional safeguards**.5960```javascript61import { spawn } from "node:child_process";62import process from "node:process";63import OpenAI from "openai";6465const client = new OpenAI();66const MAX_TIMEOUT_MS = 10_000;6768function runCommand(command, options) {69  return new Promise((resolve) => {70    let stdout = "";71    let stderr = "";72    let settled = false;73    let groupPoll;74    const child = spawn(command[0], command.slice(1), {75      ...options,76      detached: process.platform !== "win32",77      stdio: ["ignore", "pipe", "pipe"],78    });79    const finish = (suffix = "") => {80      if (settled) return;81      settled = true;82      clearTimeout(timer);83      clearTimeout(groupPoll);84      resolve(stdout + stderr + suffix);85    };86    const processGroupIsRunning = () => {87      if (process.platform === "win32" || !child.pid) return false;88      try {89        process.kill(-child.pid, 0);90        return true;91      } catch {92        return false;93      }94    };95    const finishAfterProcessGroup = (suffix) => {96      if (settled) return;97      if (processGroupIsRunning()) {98        groupPoll = setTimeout(() => finishAfterProcessGroup(suffix), 10);99      } else {100        finish(suffix);101      }102    };103    const killProcessTree = () => {104      try {105        if (process.platform !== "win32" && child.pid) {106          process.kill(-child.pid, "SIGKILL");107        } else {108          child.kill("SIGKILL");109        }110      } catch {111        child.kill("SIGKILL");112      }113      child.stdout?.destroy();114      child.stderr?.destroy();115    };116    const timer = setTimeout(() => {117      killProcessTree();118      finish("Command timed out.\n");119    }, options.timeout);120121    child.stdout?.on("data", (chunk) => {122      stdout += chunk;123    });124    child.stderr?.on("data", (chunk) => {125      stderr += chunk;126    });127    child.on("error", (error) => {128      finish(`Command failed: ${error.message}.\n`);129    });130    child.on("close", (code, signal) => {131      if (signal) {132        finishAfterProcessGroup(`Command failed with signal ${signal}.\n`);133      } else if (code !== 0) {134        finishAfterProcessGroup(`Command failed with exit code ${code}.\n`);135      } else {136        finishAfterProcessGroup("");137      }138    });139  });140}141142let response = await client.responses.create({143  model: "codex-mini-latest",144  tools: [{ type: "local_shell" }],145  parallel_tool_calls: false,146  input: "List files in the current directory.",147});148149while (true) {150  const shellCall = response.output.find(151    (item) => item.type === "local_shell_call"152  );153  if (!shellCall) break;154155  const { command, env, timeout_ms, user, working_directory } =156    shellCall.action;157  let output;158  if (user) {159    output = `Unsupported execution user: ${user}.\n`;160  } else if (command.length === 0) {161    output = "Command is empty.\n";162  } else {163    const timeout =164      timeout_ms && timeout_ms > 0165        ? Math.min(timeout_ms, MAX_TIMEOUT_MS)166        : MAX_TIMEOUT_MS;167    try {168      output = await runCommand(command, {169        cwd: working_directory ?? process.cwd(),170        env: { PATH: process.env.PATH ?? "", ...env },171        timeout,172      });173    } catch (error) {174      output = `Command failed: ${error instanceof Error ? error.message : String(error)}.\n`;175    }176  }177178  response = await client.responses.create({179    model: "codex-mini-latest",180    tools: [{ type: "local_shell" }],181    parallel_tool_calls: false,182    previous_response_id: response.id,183    input: [184      {185        type: "local_shell_call_output",186        id: shellCall.call_id,187        output,188      },189    ],190  });191}192193console.log(response.output_text);194```195196```python197import os198import signal199import subprocess200import time201from contextlib import suppress202from openai import OpenAI203204client = OpenAI()205MAX_TIMEOUT_MS = 10_000206207208def output_text(value):209    if isinstance(value, bytes):210        return value.decode(errors="replace")211    return value or ""212213214def process_group_is_running(pid):215    if os.name == "nt":216        return False217    try:218        os.killpg(pid, 0)219        return True220    except ProcessLookupError:221        return False222    except PermissionError:223        return True224225226response = client.responses.create(227    model="codex-mini-latest",228    tools=[{"type": "local_shell"}],229    parallel_tool_calls=False,230    input="List files in the current directory.",231)232233while True:234    shell_call = next(235        (item for item in response.output if item.type == "local_shell_call"),236        None,237    )238    if shell_call is None:239        break240241    action = shell_call.action242    if action.user:243        output = f"Unsupported execution user: {action.user}.\n"244    elif not action.command:245        output = "Command is empty.\n"246    else:247        timeout_ms = (248            min(action.timeout_ms, MAX_TIMEOUT_MS)249            if action.timeout_ms and action.timeout_ms > 0250            else MAX_TIMEOUT_MS251        )252        deadline = time.monotonic() + timeout_ms / 1000253        try:254            process = subprocess.Popen(255                action.command,256                cwd=action.working_directory or os.getcwd(),257                env={"PATH": os.environ.get("PATH", ""), **action.env},258                stdin=subprocess.DEVNULL,259                stdout=subprocess.PIPE,260                stderr=subprocess.PIPE,261                text=True,262                errors="replace",263                start_new_session=True,264            )265            stdout, stderr = process.communicate(266                timeout=max(deadline - time.monotonic(), 0)267            )268            while process_group_is_running(process.pid):269                remaining = deadline - time.monotonic()270                if remaining <= 0:271                    raise subprocess.TimeoutExpired(action.command, timeout_ms / 1000)272                time.sleep(min(remaining, 0.01))273            output = stdout + stderr274            if process.returncode:275                output += f"Command failed with exit code {process.returncode}.\n"276        except subprocess.TimeoutExpired as error:277            if os.name == "nt":278                process.kill()279            else:280                with suppress(ProcessLookupError):281                    os.killpg(process.pid, signal.SIGKILL)282            try:283                stdout, stderr = process.communicate(284                    timeout=max(deadline - time.monotonic(), 0)285                )286            except subprocess.TimeoutExpired as drain_error:287                if process.stdout:288                    process.stdout.close()289                if process.stderr:290                    process.stderr.close()291                stdout = output_text(292                    drain_error.stdout293                    if drain_error.stdout is not None294                    else error.stdout295                )296                stderr = output_text(297                    drain_error.stderr298                    if drain_error.stderr is not None299                    else error.stderr300                )301            output = output_text(stdout) + output_text(stderr) + "Command timed out.\n"302        except (OSError, TypeError, ValueError) as error:303            output = f"Command failed: {error}.\n"304305    output_item = {306        "type": "local_shell_call_output",307        "id": shell_call.call_id,308        "output": output,309    }310311    response = client.responses.create(312        model="codex-mini-latest",313        tools=[{"type": "local_shell"}],314        parallel_tool_calls=False,315        previous_response_id=response.id,316        input=[output_item],317    )318319print(response.output_text)320```321322```go323package main324325import (326	"bytes"327	"context"328	"errors"329	"fmt"330	"io"331	"os"332	"os/exec"333	"path/filepath"334	"sync"335	"sync/atomic"336	"syscall"337	"time"338339	"github.com/openai/openai-go/v3"340	"github.com/openai/openai-go/v3/responses"341)342343const maxCommandTimeout = 10 * time.Second344345func main() {346	client := openai.NewClient()347	tool := responses.ToolUnionParam{OfLocalShell: &responses.ToolLocalShellParam{}}348	response, err := client.Responses.New(context.Background(), responses.ResponseNewParams{349		Model:             "codex-mini-latest",350		Tools:             []responses.ToolUnionParam{tool},351		ParallelToolCalls: openai.Bool(false),352		Input: responses.ResponseNewParamsInputUnion{353			OfString: openai.String("List files in the current directory."),354		},355	})356	if err != nil {357		panic(err)358	}359360	for {361		var shellCall *responses.ResponseOutputItemLocalShellCall362		for _, item := range response.Output {363			if item.Type == "local_shell_call" {364				call := item.AsLocalShellCall()365				shellCall = &call366				break367			}368		}369		if shellCall == nil {370			break371		}372373		action := shellCall.Action374		var output []byte375		if action.User != "" {376			output = []byte(fmt.Sprintf("Unsupported execution user: %s.\n", action.User))377		} else if len(action.Command) == 0 {378			output = []byte("Command is empty.\n")379		} else {380			path := os.Getenv("PATH")381			if actionPath, ok := action.Env["PATH"]; ok {382				path = actionPath383			}384			executable, pathErr := commandPath(action.Command[0], path, action.WorkingDirectory)385			if pathErr != nil {386				output = []byte(fmt.Sprintf("Command failed: %v\n", pathErr))387			} else {388				timeout := maxCommandTimeout389				if action.TimeoutMs > 0 && action.TimeoutMs < maxCommandTimeout.Milliseconds() {390					timeout = time.Duration(action.TimeoutMs) * time.Millisecond391				}392				deadline := time.Now().Add(timeout)393				ctx, cancel := context.WithTimeout(context.Background(), timeout)394				command := exec.CommandContext(ctx, executable, action.Command[1:]...)395				command.Args[0] = action.Command[0]396				command.Dir = action.WorkingDirectory397				command.Env = []string{"PATH=" + path}398				command.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}399				for key, value := range action.Env {400					if key == "PATH" {401						continue402					}403					command.Env = append(command.Env, key+"="+value)404				}405				killProcessGroup := func() {406					if command.Process != nil {407						_ = syscall.Kill(-command.Process.Pid, syscall.SIGKILL)408					}409				}410				processGroupIsRunning := func() bool {411					return command.Process != nil && syscall.Kill(-command.Process.Pid, 0) == nil412				}413				stdout, stdoutWriter, stdoutErr := os.Pipe()414				stderr, stderrWriter, stderrErr := os.Pipe()415				if stdoutErr != nil || stderrErr != nil {416					if stdout != nil {417						_ = stdout.Close()418					}419					if stdoutWriter != nil {420						_ = stdoutWriter.Close()421					}422					if stderr != nil {423						_ = stderr.Close()424					}425					if stderrWriter != nil {426						_ = stderrWriter.Close()427					}428					output = []byte(fmt.Sprintf("Command failed: %v%v\n", stdoutErr, stderrErr))429				} else {430					command.Stdout = stdoutWriter431					command.Stderr = stderrWriter432					var combinedOutput bytes.Buffer433					var outputLock sync.Mutex434					var readers sync.WaitGroup435					readOutput := func(reader io.ReadCloser) {436						defer readers.Done()437						data, _ := io.ReadAll(reader)438						outputLock.Lock()439						_, _ = combinedOutput.Write(data)440						outputLock.Unlock()441					}442					var commandErr error443					commandErr = command.Start()444					if commandErr == nil {445						_ = stdoutWriter.Close()446						_ = stderrWriter.Close()447						readers.Add(2)448						go readOutput(stdout)449						go readOutput(stderr)450						var timedOut atomic.Bool451						remaining := time.Until(deadline)452						if remaining < 0 {453							remaining = 0454						}455						markTimedOut := func() {456							if timedOut.Swap(true) {457								return458							}459							killProcessGroup()460							_ = stdout.Close()461							_ = stderr.Close()462						}463						timer := time.AfterFunc(remaining, markTimedOut)464						commandErr = command.Wait()465						if !time.Now().Before(deadline) ||466							errors.Is(commandErr, context.DeadlineExceeded) ||467							errors.Is(ctx.Err(), context.DeadlineExceeded) {468							markTimedOut()469						}470						for processGroupIsRunning() && !timedOut.Load() {471							time.Sleep(10 * time.Millisecond)472						}473						readers.Wait()474						if !timer.Stop() || !time.Now().Before(deadline) {475							markTimedOut()476						}477						output = combinedOutput.Bytes()478						if timedOut.Load() || errors.Is(ctx.Err(), context.DeadlineExceeded) {479							killProcessGroup()480							output = append(output, "Command timed out.\n"...)481						} else if commandErr != nil {482							output = append(output, fmt.Sprintf("Command failed: %v\n", commandErr)...)483						}484					} else {485						_ = stdout.Close()486						_ = stderr.Close()487						_ = stdoutWriter.Close()488						_ = stderrWriter.Close()489						output = append(output, fmt.Sprintf("Command failed: %v\n", commandErr)...)490					}491				}492				cancel()493			}494		}495496		response, err = client.Responses.New(context.Background(), responses.ResponseNewParams{497			Model:              "codex-mini-latest",498			Tools:              []responses.ToolUnionParam{tool},499			ParallelToolCalls:  openai.Bool(false),500			PreviousResponseID: openai.String(response.ID),501			Input: responses.ResponseNewParamsInputUnion{502				OfInputItemList: []responses.ResponseInputItemUnionParam{{503					OfLocalShellCallOutput: &responses.ResponseInputItemLocalShellCallOutputParam{504						ID:     shellCall.CallID,505						Output: string(output),506					},507				}},508			},509		})510		if err != nil {511			panic(err)512		}513	}514515	fmt.Println(response.OutputText())516}517518func commandPath(command string, path string, workingDirectory string) (string, error) {519	if filepath.Base(command) != command {520		return command, nil521	}522	baseDirectory, err := filepath.Abs(workingDirectory)523	if err != nil {524		return "", err525	}526	directories := filepath.SplitList(path)527	if len(directories) == 0 {528		directories = []string{""}529	}530	for _, directory := range directories {531		if directory == "" {532			directory = "."533		}534		if !filepath.IsAbs(directory) {535			directory = filepath.Join(baseDirectory, directory)536		}537		candidate := filepath.Join(directory, command)538		info, err := os.Stat(candidate)539		if err == nil && !info.IsDir() && info.Mode()&0o111 != 0 {540			return candidate, nil541		}542	}543	return "", fmt.Errorf("command %q not found in PATH", command)544}545```546547```java548import com.openai.client.OpenAIClient;549import com.openai.client.okhttp.OpenAIOkHttpClient;550import com.openai.core.JsonValue;551import com.openai.models.responses.ResponseCreateParams;552import com.openai.models.responses.ResponseInputItem;553import java.io.IOException;554import java.io.UncheckedIOException;555import java.nio.charset.StandardCharsets;556import java.nio.file.Files;557import java.nio.file.Path;558import java.util.ArrayList;559import java.util.LinkedHashMap;560import java.util.List;561import java.util.Locale;562import java.util.Map;563import java.util.concurrent.CompletableFuture;564import java.util.concurrent.TimeUnit;565import java.util.concurrent.TimeoutException;566567ResponseCreateParams.Builder request =568    ResponseCreateParams.builder()569        .model("codex-mini-latest")570        .input("List files in the current directory.")571        .parallelToolCalls(false)572        .putAdditionalBodyProperty(573            "tools", JsonValue.from(List.of(Map.of("type", "local_shell"))));574var response = client.responses().create(request.build());575576while (true) {577  var shellCall =578      response.output().stream()579          .flatMap(item -> item.localShellCall().stream())580          .findFirst()581          .orElse(null);582  if (shellCall == null) {583    break;584  }585586  var action = shellCall.action();587  String output;588  if (action.user().isPresent()) {589    output = "Unsupported execution user: " + action.user().get() + ".\n";590  } else if (action.command().isEmpty()) {591    output = "Command is empty.\n";592  } else {593    try {594      boolean usesShellSupervisor =595          !System.getProperty("os.name").toLowerCase(Locale.ROOT).startsWith("win");596      String hostPath = System.getenv("PATH");597      String childPath = hostPath;598      Map<String, String> actionEnvironment = new LinkedHashMap<>();599      for (Map.Entry<String, com.openai.core.JsonValue> variable :600          action.env()._additionalProperties().entrySet()) {601        String value = (String) variable.getValue().asString().orElseThrow();602        actionEnvironment.put(variable.getKey(), value);603        if (variable.getKey().equals("PATH")) {604          childPath = value;605        }606      }607      List<String> command;608      if (usesShellSupervisor) {609        String supervisorShell =610            Files.isExecutable(Path.of("/bin/bash")) ? "/bin/bash" : "/bin/sh";611        command =612            new ArrayList<>(613                List.of(614                    supervisorShell,615                    "-c",616                    "set -m; child=; "617                        + "cleanup() { test -z \"$child\" || "618                        + "kill -KILL -- \"-$child\" 2>/dev/null; }; "619                        + "trap cleanup TERM INT HUP; \"$@\" & child=$!; set +m; "620                        + "wait \"$child\" 2>/dev/null; status=$?; "621                        + "while kill -0 -- \"-$child\" 2>/dev/null; do sleep 0.01; done; "622                        + "exit \"$status\"",623                    "local-shell",624                    "/usr/bin/env",625                    "-i"));626        if (childPath != null) {627          command.add("PATH=" + childPath);628        }629        for (Map.Entry<String, String> variable : actionEnvironment.entrySet()) {630          if (!variable.getKey().equals("PATH")) {631            command.add(variable.getKey() + "=" + variable.getValue());632          }633        }634        command.addAll(action.command());635      } else {636        command = new ArrayList<>(action.command());637      }638      ProcessBuilder processBuilder = new ProcessBuilder(command);639      processBuilder.directory(action.workingDirectory().map(java.io.File::new).orElse(null));640      processBuilder.environment().clear();641      if (hostPath != null) {642        processBuilder.environment().put("PATH", hostPath);643      }644      if (!usesShellSupervisor) {645        processBuilder.environment().putAll(actionEnvironment);646      }647      Process process = processBuilder.redirectErrorStream(true).start();648      process.getOutputStream().close();649      var outputFuture =650          CompletableFuture.supplyAsync(651              () -> {652                try {653                  return new String(654                      process.getInputStream().readAllBytes(), StandardCharsets.UTF_8);655                } catch (IOException error) {656                  throw new UncheckedIOException(error);657                }658              });659      long timeoutMillis =660          action661              .timeoutMs()662              .filter(timeout -> timeout > 0)663              .map(timeout -> Math.min(timeout, MAX_TIMEOUT_MILLIS))664              .orElse(MAX_TIMEOUT_MILLIS);665      long deadlineNanos = System.nanoTime() + TimeUnit.MILLISECONDS.toNanos(timeoutMillis);666      boolean finished = process.waitFor(timeoutMillis, TimeUnit.MILLISECONDS);667      if (!finished) {668        destroyProcessTree(process, usesShellSupervisor);669      }670      try {671        long remainingNanos = Math.max(1, deadlineNanos - System.nanoTime());672        output = outputFuture.get(remainingNanos, TimeUnit.NANOSECONDS);673        if (!finished) {674          output = "Command timed out.\n" + output;675        } else if (process.exitValue() != 0) {676          output += "Command failed with exit code " + process.exitValue() + ".\n";677        }678      } catch (TimeoutException error) {679        destroyProcessTree(process, usesShellSupervisor);680        process.getInputStream().close();681        output = "Command timed out.\n";682      } catch (java.util.concurrent.ExecutionException error) {683        output = "Command failed: " + error.getCause().getMessage() + ".\n";684      }685    } catch (IOException | IllegalArgumentException error) {686      output = "Command failed: " + error.getMessage() + ".\n";687    }688  }689690  response =691      client692          .responses()693          .create(694              request695                  .previousResponseId(response.id())696                  .inputOfResponse(697                      List.of(698                          ResponseInputItem.ofLocalShellCallOutput(699                              ResponseInputItem.LocalShellCallOutput.builder()700                                  .id(shellCall.callId())701                                  .output(output)702                                  .build())))703                  .build());704}705706response.output().stream()707    .flatMap(item -> item.message().stream())708    .flatMap(message -> message.content().stream())709    .flatMap(content -> content.outputText().stream())710    .forEach(text -> System.out.println(text.text()));711712private static void destroyProcessTree(Process process, boolean usesShellSupervisor) {713  process.descendants().forEach(ProcessHandle::destroyForcibly);714  if (usesShellSupervisor) {715    process.destroy();716  } else {717    process.destroyForcibly();718  }719}720```721722```ruby723require "open3"724require "openai"725require "timeout"726727client = OpenAI::Client.new728MAX_TIMEOUT_MS = 10_000729response = client.responses.create(730  model: "codex-mini-latest",731  tools: [{ type: :local_shell }],732  parallel_tool_calls: false,733  input: "List files in the current directory."734)735736loop do737  shell_call = response.output.find do |item|738    item.is_a?(OpenAI::Models::Responses::ResponseOutputItem::LocalShellCall)739  end740  break unless shell_call.is_a?(741    OpenAI::Models::Responses::ResponseOutputItem::LocalShellCall742  )743744  action = shell_call.action745  stdout = +""746  stderr = +""747  if action.user748    stderr << "Unsupported execution user: #{action.user}.\n"749  elsif action.command.empty?750    stderr << "Command is empty.\n"751  else752    begin753      executable = action.command.fetch(0)754      environment = { "PATH" => ENV.fetch("PATH", "") }.merge(action.env.transform_keys(&:to_s))755      status, timed_out = Open3.popen3(756        environment,757        [executable, executable],758        *action.command.drop(1),759        chdir: action.working_directory || Dir.pwd,760        pgroup: true,761        unsetenv_others: true762      ) do |stdin, child_stdout, child_stderr, wait_thread|763        stdin.close764        stdout_reader = Thread.new {765          begin766            child_stdout.read767          rescue768            ""769          end770        }771        stderr_reader = Thread.new {772          begin773            child_stderr.read774          rescue775            ""776          end777        }778        timeout_ms = action.timeout_ms779        timeout = if timeout_ms&.positive?780                    [timeout_ms, MAX_TIMEOUT_MS].min / 1000.0781                  else782                    MAX_TIMEOUT_MS / 1000.0783                  end784        deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + timeout785786        command_timed_out = false787        wait_status = begin788          status = Timeout.timeout(timeout) { wait_thread.value }789          remaining = deadline - Process.clock_gettime(Process::CLOCK_MONOTONIC)790          raise Timeout::Error if remaining <= 0791792          stdout << Timeout.timeout(remaining) { stdout_reader.value }793          remaining = deadline - Process.clock_gettime(Process::CLOCK_MONOTONIC)794          raise Timeout::Error if remaining <= 0795796          stderr << Timeout.timeout(remaining) { stderr_reader.value }797          group_running = proc do798            Process.kill(0, -wait_thread.pid)799            true800          rescue Errno::ESRCH801            false802          rescue Errno::EPERM803            true804          end805          while group_running.call806            remaining = deadline - Process.clock_gettime(Process::CLOCK_MONOTONIC)807            raise Timeout::Error if remaining <= 0808809            sleep [remaining, 0.01].min810          end811          status812        rescue Timeout::Error813          command_timed_out = true814          begin815            Process.kill("TERM", -wait_thread.pid)816            Process.kill("KILL", -wait_thread.pid)817          rescue Errno::ESRCH818            nil819          end820          child_stdout.close821          child_stderr.close822          stdout_reader.kill823          stderr_reader.kill824          stderr << "Command timed out.\n"825          wait_thread.value826        end827        [wait_status, command_timed_out]828      end829      exit_status = status.exitstatus830      if exit_status && !status.success? && !timed_out831        stderr << "Command failed with exit code #{exit_status}.\n"832      elsif status.signaled? && !timed_out833        stderr << "Command failed with signal #{status.termsig}.\n"834      end835    rescue SystemCallError, ArgumentError, TypeError => error836      stderr << "Command failed: #{error.message}.\n"837    end838  end839840  response = client.responses.create(841    model: "codex-mini-latest",842    tools: [{ type: :local_shell }],843    parallel_tool_calls: false,844    previous_response_id: response.id,845    input: [846      {847        type: :local_shell_call_output,848        id: shell_call.call_id,849        output: (stdout + stderr).encode("UTF-8", invalid: :replace, undef: :replace)850      }851    ]852  )853end854855puts(response.output_text)856```857858859## Best practices860861- **Sandbox or containerize** execution. Consider using Docker or a jailed user862  account.863- **Impose resource limits** (time, memory, network). The `timeout_ms`864  provided by the model is only a hint—you should enforce your own limits.865- **Filter or scrutinize** high-risk commands (for example, `rm`, `curl`, network866  utilities).867- **Log every command and its output** for auditing and debugging.868869### Error handling870871If the command fails on your side, for example, with a non-zero exit code or timeout, you can still send a `local_shell_call_output`; include the error message in the `output` field.872873The model can choose to recover or try executing a different command. If you send malformed data (for example, a missing `id`) the API returns a standard `400` validation error.
Files main
Menu

Theme