OpenAI 本地 Shell

2026-11-05 17:58 更新

弃用公告将2026年2月12日列为 codex-mini-latest 和旧版本地Shell工具的支持终止日期。新用途应选择当前的[Shell工具](<13-OpenAI Shell.md>),其请求和执行流程与以下保留的旧示例不同。

本地Shell让智能体在你或用户提供的机器上运行命令。旧工具为Codex CLI与 codex-mini-latest 设计,命令由自己的运行时执行,因此实际运行哪些命令完全由你控制。API只返回执行指令,不在OpenAI基础设施上执行命令。

以下是来源保留的历史能力范围:通过Responses API用于 codex-mini-latest,不支持其他模型或Chat Completions API。该范围不表示旧工具当前仍受支持。

执行任意Shell命令具有风险。在转交给系统Shell前,应使用沙箱或严格的允许/拒绝列表。可参考Codex CLI的实现。

工作原理

模型发出Shell命令,应用在本地机器执行,再把结果返回模型。这个持续循环让模型进行构建、测试和运行。

应用需监听 local_shell_call 输出项,执行其中请求的命令。官方强烈建议把执行放在沙箱中,避免非预期命令运行。

集成过程:

  1. 请求模型时加入 local_shell 工具。
  2. 检查响应中的 local_shell_call,它包含 exec 等操作及待执行命令。
  3. 在自己控制的本地环境执行操作。
  4. 将命令输出返回模型。
  5. 把更新后的状态作为 local_shell_call_output 发送后续请求,循环到模型不再请求操作,或应用决定停止。

示例工作流

以下保留各SDK语言的最小请求与响应循环。为简洁起见,来源省略了生产级沙箱和安全检查;生产环境不能在缺少额外防护时执行不可信命令。

来源示例1(javascript)

程序中的自然语言译释74:

列出当前目录中的文件。

程序中的自然语言译释75:

命令失败,后面附退出码。

// [中文] 历史local_shell循环在应用本地执行命令并回传输出;旧工具已停止支持,不与当前shell_call混用。
import { spawn } from "node:child_process";
import process from "node:process";
import OpenAI from "openai";

const client = new OpenAI();
const MAX_TIMEOUT_MS = 10_000;

function runCommand(command, options) {
  return new Promise((resolve) => {
    let stdout = "";
    let stderr = "";
    let settled = false;
    let groupPoll;
    const child = spawn(command[0], command.slice(1), {
      ...options,
      detached: process.platform !== "win32",
      stdio: ["ignore", "pipe", "pipe"],
    });
    const finish = (suffix = "") => {
      if (settled) return;
      settled = true;
      clearTimeout(timer);
      clearTimeout(groupPoll);
      resolve(stdout + stderr + suffix);
    };
    const processGroupIsRunning = () => {
      if (process.platform === "win32" || !child.pid) return false;
      try {
        process.kill(-child.pid, 0);
        return true;
      } catch {
        return false;
      }
    };
    const finishAfterProcessGroup = (suffix) => {
      if (settled) return;
      if (processGroupIsRunning()) {
        groupPoll = setTimeout(() => finishAfterProcessGroup(suffix), 10);
      } else {
        finish(suffix);
      }
    };
    const killProcessTree = () => {
      try {
        if (process.platform !== "win32" && child.pid) {
          process.kill(-child.pid, "SIGKILL");
        } else {
          child.kill("SIGKILL");
        }
      } catch {
        child.kill("SIGKILL");
      }
      child.stdout?.destroy();
      child.stderr?.destroy();
    };
    const timer = setTimeout(() => {
      killProcessTree();
      finish("Command timed out.\n");
    }, options.timeout);

    child.stdout?.on("data", (chunk) => {
      stdout += chunk;
    });
    child.stderr?.on("data", (chunk) => {
      stderr += chunk;
    });
    child.on("error", (error) => {
      finish(`Command failed: ${error.message}.\n`);
    });
    child.on("close", (code, signal) => {
      if (signal) {
        finishAfterProcessGroup(`Command failed with signal ${signal}.\n`);
      } else if (code !== 0) {
        finishAfterProcessGroup(`Command failed with exit code ${code}.\n`);
      } else {
        finishAfterProcessGroup("");
      }
    });
  });
}

let response = await client.responses.create({
  model: "codex-mini-latest",
  tools: [{ type: "local_shell" }],
  parallel_tool_calls: false,
  input: "List files in the current directory.",
});

while (true) {
  const shellCall = response.output.find(
    (item) => item.type === "local_shell_call"
  );
  if (!shellCall) break;

  const { command, env, timeout_ms, user, working_directory } =
    shellCall.action;
  let output;
  if (user) {
    output = `Unsupported execution user: ${user}.\n`;
  } else if (command.length === 0) {
    output = "Command is empty.\n";
  } else {
    const timeout =
      timeout_ms && timeout_ms > 0
        ? Math.min(timeout_ms, MAX_TIMEOUT_MS)
        : MAX_TIMEOUT_MS;
    try {
      output = await runCommand(command, {
        cwd: working_directory ?? process.cwd(),
        env: { PATH: process.env.PATH ?? "", ...env },
        timeout,
      });
    } catch (error) {
      output = `Command failed: ${error instanceof Error ? error.message : String(error)}.\n`;
    }
  }

  response = await client.responses.create({
    model: "codex-mini-latest",
    tools: [{ type: "local_shell" }],
    parallel_tool_calls: false,
    previous_response_id: response.id,
    input: [
      {
        type: "local_shell_call_output",
        id: shellCall.call_id,
        output,
      },
    ],
  });
}

console.log(response.output_text);

来源示例2(python)

本段复用的中文释义:译释74、译释75。

## [中文] 历史local_shell循环在应用本地执行命令并回传输出;旧工具已停止支持,不与当前shell_call混用。
import os
import signal
import subprocess
import time
from contextlib import suppress
from openai import OpenAI

client = OpenAI()
MAX_TIMEOUT_MS = 10_000


def output_text(value):
    if isinstance(value, bytes):
        return value.decode(errors="replace")
    return value or ""


def process_group_is_running(pid):
    if os.name == "nt":
        return False
    try:
        os.killpg(pid, 0)
        return True
    except ProcessLookupError:
        return False
    except PermissionError:
        return True


response = client.responses.create(
    model="codex-mini-latest",
    tools=[{"type": "local_shell"}],
    parallel_tool_calls=False,
    input="List files in the current directory.",
)

while True:
    shell_call = next(
        (item for item in response.output if item.type == "local_shell_call"),
        None,
    )
    if shell_call is None:
        break

    action = shell_call.action
    if action.user:
        output = f"Unsupported execution user: {action.user}.\n"
    elif not action.command:
        output = "Command is empty.\n"
    else:
        timeout_ms = (
            min(action.timeout_ms, MAX_TIMEOUT_MS)
            if action.timeout_ms and action.timeout_ms > 0
            else MAX_TIMEOUT_MS
        )
        deadline = time.monotonic() + timeout_ms / 1000
        try:
            process = subprocess.Popen(
                action.command,
                cwd=action.working_directory or os.getcwd(),
                env={"PATH": os.environ.get("PATH", ""), **action.env},
                stdin=subprocess.DEVNULL,
                stdout=subprocess.PIPE,
                stderr=subprocess.PIPE,
                text=True,
                errors="replace",
                start_new_session=True,
            )
            stdout, stderr = process.communicate(
                timeout=max(deadline - time.monotonic(), 0)
            )
            while process_group_is_running(process.pid):
                remaining = deadline - time.monotonic()
                if remaining <= 0:
                    raise subprocess.TimeoutExpired(action.command, timeout_ms / 1000)
                time.sleep(min(remaining, 0.01))
            output = stdout + stderr
            if process.returncode:
                output += f"Command failed with exit code {process.returncode}.\n"
        except subprocess.TimeoutExpired as error:
            if os.name == "nt":
                process.kill()
            else:
                with suppress(ProcessLookupError):
                    os.killpg(process.pid, signal.SIGKILL)
            try:
                stdout, stderr = process.communicate(
                    timeout=max(deadline - time.monotonic(), 0)
                )
            except subprocess.TimeoutExpired as drain_error:
                if process.stdout:
                    process.stdout.close()
                if process.stderr:
                    process.stderr.close()
                stdout = output_text(
                    drain_error.stdout
                    if drain_error.stdout is not None
                    else error.stdout
                )
                stderr = output_text(
                    drain_error.stderr
                    if drain_error.stderr is not None
                    else error.stderr
                )
            output = output_text(stdout) + output_text(stderr) + "Command timed out.\n"
        except (OSError, TypeError, ValueError) as error:
            output = f"Command failed: {error}.\n"

    output_item = {
        "type": "local_shell_call_output",
        "id": shell_call.call_id,
        "output": output,
    }

    response = client.responses.create(
        model="codex-mini-latest",
        tools=[{"type": "local_shell"}],
        parallel_tool_calls=False,
        previous_response_id=response.id,
        input=[output_item],
    )

print(response.output_text)

来源示例3(go)

本段复用的中文释义:译释74。

// [中文] 历史local_shell循环在应用本地执行命令并回传输出;旧工具已停止支持,不与当前shell_call混用。
package main

import (
    "bytes"
    "context"
    "errors"
    "fmt"
    "io"
    "os"
    "os/exec"
    "path/filepath"
    "sync"
    "sync/atomic"
    "syscall"
    "time"

    "github.com/openai/openai-go/v3"
    "github.com/openai/openai-go/v3/responses"
)

const maxCommandTimeout = 10 * time.Second

func main() {
    client := openai.NewClient()
    tool := responses.ToolUnionParam{OfLocalShell: &responses.ToolLocalShellParam{}}
    response, err := client.Responses.New(context.Background(), responses.ResponseNewParams{
        Model:             "codex-mini-latest",
        Tools:             []responses.ToolUnionParam{tool},
        ParallelToolCalls: openai.Bool(false),
        Input: responses.ResponseNewParamsInputUnion{
            OfString: openai.String("List files in the current directory."),
        },
    })
    if err != nil {
        panic(err)
    }

    for {
        var shellCall *responses.ResponseOutputItemLocalShellCall
        for _, item := range response.Output {
            if item.Type == "local_shell_call" {
                call := item.AsLocalShellCall()
                shellCall = &call
                break
            }
        }
        if shellCall == nil {
            break
        }

        action := shellCall.Action
        var output []byte
        if action.User != "" {
            output = []byte(fmt.Sprintf("Unsupported execution user: %s.\n", action.User))
        } else if len(action.Command) == 0 {
            output = []byte("Command is empty.\n")
        } else {
            path := os.Getenv("PATH")
            if actionPath, ok := action.Env["PATH"]; ok {
                path = actionPath
            }
            executable, pathErr := commandPath(action.Command[0], path, action.WorkingDirectory)
            if pathErr != nil {
                output = []byte(fmt.Sprintf("Command failed: %v\n", pathErr))
            } else {
                timeout := maxCommandTimeout
                if action.TimeoutMs > 0 && action.TimeoutMs < maxCommandTimeout.Milliseconds() {
                    timeout = time.Duration(action.TimeoutMs) * time.Millisecond
                }
                deadline := time.Now().Add(timeout)
                ctx, cancel := context.WithTimeout(context.Background(), timeout)
                command := exec.CommandContext(ctx, executable, action.Command[1:]...)
                command.Args[0] = action.Command[0]
                command.Dir = action.WorkingDirectory
                command.Env = []string{"PATH=" + path}
                command.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
                for key, value := range action.Env {
                    if key == "PATH" {
                        continue
                    }
                    command.Env = append(command.Env, key+"="+value)
                }
                killProcessGroup := func() {
                    if command.Process != nil {
                        _ = syscall.Kill(-command.Process.Pid, syscall.SIGKILL)
                    }
                }
                processGroupIsRunning := func() bool {
                    return command.Process != nil && syscall.Kill(-command.Process.Pid, 0) == nil
                }
                stdout, stdoutWriter, stdoutErr := os.Pipe()
                stderr, stderrWriter, stderrErr := os.Pipe()
                if stdoutErr != nil || stderrErr != nil {
                    if stdout != nil {
                        _ = stdout.Close()
                    }
                    if stdoutWriter != nil {
                        _ = stdoutWriter.Close()
                    }
                    if stderr != nil {
                        _ = stderr.Close()
                    }
                    if stderrWriter != nil {
                        _ = stderrWriter.Close()
                    }
                    output = []byte(fmt.Sprintf("Command failed: %v%v\n", stdoutErr, stderrErr))
                } else {
                    command.Stdout = stdoutWriter
                    command.Stderr = stderrWriter
                    var combinedOutput bytes.Buffer
                    var outputLock sync.Mutex
                    var readers sync.WaitGroup
                    readOutput := func(reader io.ReadCloser) {
                        defer readers.Done()
                        data, _ := io.ReadAll(reader)
                        outputLock.Lock()
                        _, _ = combinedOutput.Write(data)
                        outputLock.Unlock()
                    }
                    var commandErr error
                    commandErr = command.Start()
                    if commandErr == nil {
                        _ = stdoutWriter.Close()
                        _ = stderrWriter.Close()
                        readers.Add(2)
                        go readOutput(stdout)
                        go readOutput(stderr)
                        var timedOut atomic.Bool
                        remaining := time.Until(deadline)
                        if remaining < 0 {
                            remaining = 0
                        }
                        markTimedOut := func() {
                            if timedOut.Swap(true) {
                                return
                            }
                            killProcessGroup()
                            _ = stdout.Close()
                            _ = stderr.Close()
                        }
                        timer := time.AfterFunc(remaining, markTimedOut)
                        commandErr = command.Wait()
                        if !time.Now().Before(deadline) ||
                            errors.Is(commandErr, context.DeadlineExceeded) ||
                            errors.Is(ctx.Err(), context.DeadlineExceeded) {
                            markTimedOut()
                        }
                        for processGroupIsRunning() && !timedOut.Load() {
                            time.Sleep(10 * time.Millisecond)
                        }
                        readers.Wait()
                        if !timer.Stop() || !time.Now().Before(deadline) {
                            markTimedOut()
                        }
                        output = combinedOutput.Bytes()
                        if timedOut.Load() || errors.Is(ctx.Err(), context.DeadlineExceeded) {
                            killProcessGroup()
                            output = append(output, "Command timed out.\n"...)
                        } else if commandErr != nil {
                            output = append(output, fmt.Sprintf("Command failed: %v\n", commandErr)...)
                        }
                    } else {
                        _ = stdout.Close()
                        _ = stderr.Close()
                        _ = stdoutWriter.Close()
                        _ = stderrWriter.Close()
                        output = append(output, fmt.Sprintf("Command failed: %v\n", commandErr)...)
                    }
                }
                cancel()
            }
        }

        response, err = client.Responses.New(context.Background(), responses.ResponseNewParams{
            Model:              "codex-mini-latest",
            Tools:              []responses.ToolUnionParam{tool},
            ParallelToolCalls:  openai.Bool(false),
            PreviousResponseID: openai.String(response.ID),
            Input: responses.ResponseNewParamsInputUnion{
                OfInputItemList: []responses.ResponseInputItemUnionParam{{
                    OfLocalShellCallOutput: &responses.ResponseInputItemLocalShellCallOutputParam{
                        ID:     shellCall.CallID,
                        Output: string(output),
                    },
                }},
            },
        })
        if err != nil {
            panic(err)
        }
    }

    fmt.Println(response.OutputText())
}

func commandPath(command string, path string, workingDirectory string) (string, error) {
    if filepath.Base(command) != command {
        return command, nil
    }
    baseDirectory, err := filepath.Abs(workingDirectory)
    if err != nil {
        return "", err
    }
    directories := filepath.SplitList(path)
    if len(directories) == 0 {
        directories = []string{""}
    }
    for _, directory := range directories {
        if directory == "" {
            directory = "."
        }
        if !filepath.IsAbs(directory) {
            directory = filepath.Join(baseDirectory, directory)
        }
        candidate := filepath.Join(directory, command)
        info, err := os.Stat(candidate)
        if err == nil && !info.IsDir() && info.Mode()&0o111 != 0 {
            return candidate, nil
        }
    }
    return "", fmt.Errorf("command %q not found in PATH", command)
}

来源示例4(java)

本段复用的中文释义:译释74、译释75。

// [中文] 历史local_shell循环在应用本地执行命令并回传输出;旧工具已停止支持,不与当前shell_call混用。
import com.openai.client.OpenAIClient;
import com.openai.client.okhttp.OpenAIOkHttpClient;
import com.openai.core.JsonValue;
import com.openai.models.responses.ResponseCreateParams;
import com.openai.models.responses.ResponseInputItem;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;

ResponseCreateParams.Builder request =
    ResponseCreateParams.builder()
        .model("codex-mini-latest")
        .input("List files in the current directory.")
        .parallelToolCalls(false)
        .putAdditionalBodyProperty(
            "tools", JsonValue.from(List.of(Map.of("type", "local_shell"))));
var response = client.responses().create(request.build());

while (true) {
  var shellCall =
      response.output().stream()
          .flatMap(item -> item.localShellCall().stream())
          .findFirst()
          .orElse(null);
  if (shellCall == null) {
    break;
  }

  var action = shellCall.action();
  String output;
  if (action.user().isPresent()) {
    output = "Unsupported execution user: " + action.user().get() + ".\n";
  } else if (action.command().isEmpty()) {
    output = "Command is empty.\n";
  } else {
    try {
      boolean usesShellSupervisor =
          !System.getProperty("os.name").toLowerCase(Locale.ROOT).startsWith("win");
      String hostPath = System.getenv("PATH");
      String childPath = hostPath;
      Map<String, String> actionEnvironment = new LinkedHashMap<>();
      for (Map.Entry<String, com.openai.core.JsonValue> variable :
          action.env()._additionalProperties().entrySet()) {
        String value = (String) variable.getValue().asString().orElseThrow();
        actionEnvironment.put(variable.getKey(), value);
        if (variable.getKey().equals("PATH")) {
          childPath = value;
        }
      }
      List<String> command;
      if (usesShellSupervisor) {
        String supervisorShell =
            Files.isExecutable(Path.of("/bin/bash")) ? "/bin/bash" : "/bin/sh";
        command =
            new ArrayList<>(
                List.of(
                    supervisorShell,
                    "-c",
                    "set -m; child=; "
                        + "cleanup() { test -z \"$child\" || "
                        + "kill -KILL -- \"-$child\" 2>/dev/null; }; "
                        + "trap cleanup TERM INT HUP; \"$@\" & child=$!; set +m; "
                        + "wait \"$child\" 2>/dev/null; status=$?; "
                        + "while kill -0 -- \"-$child\" 2>/dev/null; do sleep 0.01; done; "
                        + "exit \"$status\"",
                    "local-shell",
                    "/usr/bin/env",
                    "-i"));
        if (childPath != null) {
          command.add("PATH=" + childPath);
        }
        for (Map.Entry<String, String> variable : actionEnvironment.entrySet()) {
          if (!variable.getKey().equals("PATH")) {
            command.add(variable.getKey() + "=" + variable.getValue());
          }
        }
        command.addAll(action.command());
      } else {
        command = new ArrayList<>(action.command());
      }
      ProcessBuilder processBuilder = new ProcessBuilder(command);
      processBuilder.directory(action.workingDirectory().map(java.io.File::new).orElse(null));
      processBuilder.environment().clear();
      if (hostPath != null) {
        processBuilder.environment().put("PATH", hostPath);
      }
      if (!usesShellSupervisor) {
        processBuilder.environment().putAll(actionEnvironment);
      }
      Process process = processBuilder.redirectErrorStream(true).start();
      process.getOutputStream().close();
      var outputFuture =
          CompletableFuture.supplyAsync(
              () -> {
                try {
                  return new String(
                      process.getInputStream().readAllBytes(), StandardCharsets.UTF_8);
                } catch (IOException error) {
                  throw new UncheckedIOException(error);
                }
              });
      long timeoutMillis =
          action
              .timeoutMs()
              .filter(timeout -> timeout > 0)
              .map(timeout -> Math.min(timeout, MAX_TIMEOUT_MILLIS))
              .orElse(MAX_TIMEOUT_MILLIS);
      long deadlineNanos = System.nanoTime() + TimeUnit.MILLISECONDS.toNanos(timeoutMillis);
      boolean finished = process.waitFor(timeoutMillis, TimeUnit.MILLISECONDS);
      if (!finished) {
        destroyProcessTree(process, usesShellSupervisor);
      }
      try {
        long remainingNanos = Math.max(1, deadlineNanos - System.nanoTime());
        output = outputFuture.get(remainingNanos, TimeUnit.NANOSECONDS);
        if (!finished) {
          output = "Command timed out.\n" + output;
        } else if (process.exitValue() != 0) {
          output += "Command failed with exit code " + process.exitValue() + ".\n";
        }
      } catch (TimeoutException error) {
        destroyProcessTree(process, usesShellSupervisor);
        process.getInputStream().close();
        output = "Command timed out.\n";
      } catch (java.util.concurrent.ExecutionException error) {
        output = "Command failed: " + error.getCause().getMessage() + ".\n";
      }
    } catch (IOException | IllegalArgumentException error) {
      output = "Command failed: " + error.getMessage() + ".\n";
    }
  }

  response =
      client
          .responses()
          .create(
              request
                  .previousResponseId(response.id())
                  .inputOfResponse(
                      List.of(
                          ResponseInputItem.ofLocalShellCallOutput(
                              ResponseInputItem.LocalShellCallOutput.builder()
                                  .id(shellCall.callId())
                                  .output(output)
                                  .build())))
                  .build());
}

response.output().stream()
    .flatMap(item -> item.message().stream())
    .flatMap(message -> message.content().stream())
    .flatMap(content -> content.outputText().stream())
    .forEach(text -> System.out.println(text.text()));

private static void destroyProcessTree(Process process, boolean usesShellSupervisor) {
  process.descendants().forEach(ProcessHandle::destroyForcibly);
  if (usesShellSupervisor) {
    process.destroy();
  } else {
    process.destroyForcibly();
  }
}

来源示例5(ruby)

本段复用的中文释义:译释74、译释75。

## [中文] 历史local_shell循环在应用本地执行命令并回传输出;旧工具已停止支持,不与当前shell_call混用。
require "open3"
require "openai"
require "timeout"

client = OpenAI::Client.new
MAX_TIMEOUT_MS = 10_000
response = client.responses.create(
  model: "codex-mini-latest",
  tools: [{ type: :local_shell }],
  parallel_tool_calls: false,
  input: "List files in the current directory."
)

loop do
  shell_call = response.output.find do |item|
    item.is_a?(OpenAI::Models::Responses::ResponseOutputItem::LocalShellCall)
  end
  break unless shell_call.is_a?(
    OpenAI::Models::Responses::ResponseOutputItem::LocalShellCall
  )

  action = shell_call.action
  stdout = +""
  stderr = +""
  if action.user
    stderr << "Unsupported execution user: #{action.user}.\n"
  elsif action.command.empty?
    stderr << "Command is empty.\n"
  else
    begin
      executable = action.command.fetch(0)
      environment = { "PATH" => ENV.fetch("PATH", "") }.merge(action.env.transform_keys(&:to_s))
      status, timed_out = Open3.popen3(
        environment,
        [executable, executable],
        *action.command.drop(1),
        chdir: action.working_directory || Dir.pwd,
        pgroup: true,
        unsetenv_others: true
      ) do |stdin, child_stdout, child_stderr, wait_thread|
        stdin.close
        stdout_reader = Thread.new {
          begin
            child_stdout.read
          rescue
            ""
          end
        }
        stderr_reader = Thread.new {
          begin
            child_stderr.read
          rescue
            ""
          end
        }
        timeout_ms = action.timeout_ms
        timeout = if timeout_ms&.positive?
                    [timeout_ms, MAX_TIMEOUT_MS].min / 1000.0
                  else
                    MAX_TIMEOUT_MS / 1000.0
                  end
        deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + timeout

        command_timed_out = false
        wait_status = begin
          status = Timeout.timeout(timeout) { wait_thread.value }
          remaining = deadline - Process.clock_gettime(Process::CLOCK_MONOTONIC)
          raise Timeout::Error if remaining <= 0

          stdout << Timeout.timeout(remaining) { stdout_reader.value }
          remaining = deadline - Process.clock_gettime(Process::CLOCK_MONOTONIC)
          raise Timeout::Error if remaining <= 0

          stderr << Timeout.timeout(remaining) { stderr_reader.value }
          group_running = proc do
            Process.kill(0, -wait_thread.pid)
            true
          rescue Errno::ESRCH
            false
          rescue Errno::EPERM
            true
          end
          while group_running.call
            remaining = deadline - Process.clock_gettime(Process::CLOCK_MONOTONIC)
            raise Timeout::Error if remaining <= 0

            sleep [remaining, 0.01].min
          end
          status
        rescue Timeout::Error
          command_timed_out = true
          begin
            Process.kill("TERM", -wait_thread.pid)
            Process.kill("KILL", -wait_thread.pid)
          rescue Errno::ESRCH
            nil
          end
          child_stdout.close
          child_stderr.close
          stdout_reader.kill
          stderr_reader.kill
          stderr << "Command timed out.\n"
          wait_thread.value
        end
        [wait_status, command_timed_out]
      end
      exit_status = status.exitstatus
      if exit_status && !status.success? && !timed_out
        stderr << "Command failed with exit code #{exit_status}.\n"
      elsif status.signaled? && !timed_out
        stderr << "Command failed with signal #{status.termsig}.\n"
      end
    rescue SystemCallError, ArgumentError, TypeError => error
      stderr << "Command failed: #{error.message}.\n"
    end
  end

  response = client.responses.create(
    model: "codex-mini-latest",
    tools: [{ type: :local_shell }],
    parallel_tool_calls: false,
    previous_response_id: response.id,
    input: [
      {
        type: :local_shell_call_output,
        id: shell_call.call_id,
        output: (stdout + stderr).encode("UTF-8", invalid: :replace, undef: :replace)
      }
    ]
  )
end

puts(response.output_text)

最佳实践

  • 把执行放入沙箱或容器,例如Docker或受限用户账户。
  • 限制时间、内存和网络资源。模型提供的 timeout_ms 仅为提示,应用必须执行自己的限制。
  • 过滤或审查高风险命令,例如删除命令、curl 和网络工具。
  • 记录每条命令及其输出,用于审计与排错。

错误处理

本地执行失败,例如退出码非零或超时,仍可返回 local_shell_call_output,在 output 中包含错误信息。模型可以尝试恢复或执行其他命令。若返回的数据格式错误,例如缺少 id,API会返回标准的400验证错误。

以上内容是否对您有帮助:
在线笔记
App下载
App下载

扫描二维码

下载编程狮App

公众号
微信公众号

编程狮公众号