Local shell
Local shell (로컬 셸)
로컬 셸(local shell) 도구는 이제 구식이에요. 새 용도에서는 GPT-5.1과 함께 shell 도구를 사용하세요. 자세히 알아보기.
로컬 셸은 여러분이나 사용자가 제공한 머신에서 에이전트가 로컬로 셸 명령을 실행하게 해주는 도구예요. Codex CLI와 codex-mini-latest와 함께 동작하도록 설계됐어요. 명령은 여러분의 런타임 안에서 실행되므로 실제로 어떤 명령이 실행되는지는 전적으로 여러분이 통제해요. API는 지시만 돌려주며 OpenAI 인프라에서는 실행하지 않아요.
로컬 셸은 codex-mini-latest와 함께 쓰기 위해 Responses API를 통해 제공돼요. 다른 모델이나 Chat Completions API에서는 사용할 수 없어요.
임의의 셸 명령을 실행하는 것은 위험할 수 있어요. 명령을 시스템 셸로 전달하기 전에 항상 실행을 샌드박싱하거나 엄격한 허용 목록(allowlist)·차단 목록(deny list)을 추가하세요.
참조 구현은 Codex CLI를 보세요.
출처: 문서
본문
동작 방식
로컬 셸 도구는 에이전트가 터미널에 접근한 상태로 연속 루프를 돌 수 있게 해줘요.
모델이 셸 명령을 보내면, 여러분의 코드가 그 명령을 로컬 머신에서 실행하고 결과를 모델로 돌려줘요. 이 루프를 통해 모델은 추가 사용자 개입 없이 build-test-run 사이클을 완료할 수 있어요.
여러분의 코드는 local_shell_call 출력 항목을 듣고 그 안의 명령을 실행하는 루프를 구현해야 해요. 예기치 않은 명령이 실행되지 않도록 실행을 샌드박싱하는 것을 강력히 권장해요.
로컬 셸 도구 통합
애플리케이션에 로컬 셸 도구를 통합하려면 따라야 할 높은 수준의 단계는 다음과 같아요.
- 모델에 요청 보내기: 사용 가능한 도구에
local_shell도구를 포함시켜요. - 모델 응답 받기: 응답에
local_shell_call항목이 있는지 확인해요. 이 도구 호출에는exec같은 action과 실행할 명령이 담겨 있어요. - 요청된 액션 실행: 여러분이 통제하는 로컬 환경에서 명령을 실행해요.
- 액션 출력 반환: 액션을 실행한 뒤 명령 출력을 모델로 돌려줘요.
- 반복: 업데이트된 상태를
local_shell_call_output으로 담아 새 요청을 보내고, 모델이 액션을 요청하지 않거나 여러분이 중단하기로 할 때까지 이 루프를 반복해요.
예제 워크플로
아래는 요청/응답 루프를 보여주는 최소 예제예요. 언어를 선택하면 해당 SDK의 동등한 워크플로를 볼 수 있어요. 간결성을 위해 프로덕션급 샌드박싱과 보안 검사는 생략했어요. 추가 안전장치 없이 프로덕션에서 신뢰할 수 없는 명령을 실행하지 마세요.
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);
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)
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)
}
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();
}
}
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나 제한된 사용자 계정(jailed user account)을 고려해보세요.
- 리소스 한도를 적용하세요 (시간, 메모리, 네트워크). 모델이 주는
timeout_ms는 단지 힌트일 뿐이므로, 여러분만의 한도를 직접 적용해야 해요. - 고위험 명령(예:
rm,curl, 네트워크 유틸리티)을 필터링하거나 면밀히 검토하세요. - 모든 명령과 출력을 기록해서 감사(auditing)와 디버깅에 사용하세요.
오류 처리
명령이 여러분 쪽에서 실패한 경우(예: 0이 아닌 종료 코드나 타임아웃)에도 여전히 local_shell_call_output을 보낼 수 있어요. 오류 메시지를 output 필드에 포함하면 돼요.
모델은 복구하거나 다른 명령 실행을 시도하도록 선택할 수 있어요. 잘못된 데이터(예: 누락된 id)를 보내면 API가 표준 400 검증 오류를 반환해요.
더 알아보기 (Learn more)
- Shell 도구 가이드에서 최신 셸 도구를 확인하세요.
- Codex CLI 참조 구현을 살펴보세요.
codex-mini-latest모델 문서를 참고하세요.