Shell
Shell
이 문서에서는 Shell capability를 소개해요. 에이전트에 셸 명령 실행 능력, 허용/거부 제어, 환경 스크러빙, 관리되는 백그라운드 프로세스를 제공해요. 작업 디렉터리에 뿌리를 둔 명령 실행 도구를 노출하고 에이전트 실행이 끝나면 백그라운드 프로세스를 자동으로 정리해요.
출처: 문서
본문
Shell은 에이전트에 셸 명령 실행 능력을 주며, 허용/거부 제어, 환경 스크러빙, 관리되는 백그라운드 프로세스를 제공해요. 작업 디렉터리에 뿌리를 둔 명령 실행 도구를 노출하고 에이전트 실행이 끝나면 백그라운드 프로세스를 자동으로 정리해요.
Pydantic AI Harness가 0.x 릴리스인 동안에는 minor 릴리스 사이에 API가 바뀔 수 있어요. 그럴 때는 deprecation 경고와 릴리스 노트 마이그레이션 안내가 (당신이나 당신의 에이전트에게) 정확히 업그레이드 방법을 알려줘요. 버전 정책을 참고하세요.
The problem
에이전트는 자주 빌드, 테스트 스위트, 린터, 또는 빠른 grep을 실행해야 해요. 하위 프로세스 처리를 연결하는 것 — 출력 스트리밍, 타임아웃, 잘림, 폭주하는 프로세스 죽이기, 실행 끝의 백그라운드 작업 정리 — 은 모든 에이전트가 재발명하는 까다로운 보일러플레이트예요.
Shell은 그 배관을 단일 capability로 묶어요. 구성 가능한 허용/거부 목록, 유용한 꼬리를 유지하도록 조정된 출력 잘림, 선택적 고정 작업 디렉터리, 호스트 비밀을 생성된 명령에서 유지할 수 있는 환경 제어, 실행이 끝나면 백그라운드 프로세스를 자동 정리하는 기능.
Usage
작업 디렉터리와 함께 Shell을 구성하고 capabilities 파라미터를 통해 Agent에 전달하세요:
from pydantic_ai import Agent
from pydantic_ai_harness import Shell
agent = Agent(
'anthropic:claude-sonnet-4-6',
capabilities=[Shell(cwd='./workspace', allowed_commands=['ls', 'cat', 'rg'])],
)
result = agent.run_sync('List the Python files and summarize the largest one.')
print(result.output)
기본적으로 Shell은 내장 파괴적 명령 denylist가 활성화된 현재 디렉터리에서 실행돼요 — Shell() 단독은 동작하는(관대하긴 하지만) 구성이에요.
Tools
Shell은 기본적으로 네 개의 실행 범위 도구와, 옵트인하는 영속 shell 도구를 제공해요:
Tool
Purpose
run_command
명령을 동기로 실행하고 라벨된 stdout/stderr와 종료 코드를 반환. 호출별 또는 기본 타임아웃을 존중.
start_command
장시간 실행 명령(서버, 워처)을 백그라운드로 시작. ID를 반환.
check_command
백그라운드 명령의 상태와 누적된 출력을 보고.
stop_command
백그라운드 명령을 종료하고 최종 출력을 반환.
shell
옵트인: 실행을 오래 사는 명령을 foreground(최대 timeout까지 기다린 뒤 여전히 실행 중인 프로세스를 돌려줌) 또는 background 모드로 실행. PID와 출력 로그 및 JSON 상태 파일 경로를 반환.
run_command는 단일 호출에 대해 default_timeout을 재정의하는 선택적 timeout_seconds 인자를 받아들여요. check_command와 stop_command는 start_command가 반환한 command_id 문자열을 받아요.
출력은 [stdout]/[stderr] 표식과 0이 아닌 종료 시 [exit code: N] 줄로 라벨돼요. max_output_chars를 초과하면 꼬리가 유지되고(head가 버려짐), 그래서 오류, 스택 트레이스, [stderr] 섹션 — 모두 끝에 붙는 것들 — 이 잘림에서 살아남아요. 백그라운드 명령 상태와 종료 메타데이터는 캡처된 출력을 따라가므로 유지된 꼬리에 남아요.
Command controls
서로 배타적인 두 목록이 실행할 수 있는 실행 파일을 결정하며, 셸 연산자와 대화형 명령용 필터도 있어요:
Field
Effect
allowed_commands
비어 있지 않으면 이 실행 파일들만 실행될 수 있어요(allowlist).
denied_commands
이 실행 파일들은 항상 거부돼요(denylist).
denied_operators
존재할 때 거부되는 셸 연산자(예: >, >>, |).
allow_interactive
False(기본값)이면 TTY를 기대하는 명령(vi, sudo, ssh, ...)이 차단돼요.
allowed_commands와 denied_commands는 상호 배타적이에요 — 하나만 설정하세요, 둘 다는 말고. 둘 다 비어 있지 않은 값을 설정하면 툴셋이 구성될 때 ValueError가 발생해요. denied_commands는 기본적으로 파괴적 명령 목록(rm, rmdir, mkfs, dd, format, shutdown, reboot, halt, poweroff, init)이고, 비활성화하려면 빈 목록을 전달하세요. 실행 파일 이름은 shlex로 추출되므로 인자가 검사를 우회하지 않아요.
빈 allowed_commands 컬렉션은 allowlist 모드를 선택하지 않아요. 구성된 denied_commands가 계속 활성화되고, 생략되면 내장 denylist예요. 명령 이름 필터링을 비활성화하려면 denied_commands=[]를 전달하세요.
거부된 명령은 하드 오류가 아닌 ModelRetry로 모델에 표면화돼요. 실행이 계속되고 모델이 대신 허용된 명령을 고를 수 있어요. 모델이 조치할 수 있는 다른 모든 실패도 마찬가지예요. 이전 명령이 삭제했거나 파일로 바꾼 작업 디렉터리, 그리고 NUL 바이트를 지니거나 운영 체제가 인코딩할 수 없는 문자를 포함해 운영 체제가 생성하기를 거부하는 명령. 모델이 할 수 있는 것이 없는 실패는 여전히 실행을 중단해요. 프로세스를 할당할 수 없는 호스트, 플랫폼의 결합 크기 한도를 초과하는 인자나 환경, 애플리케이션이 공급하는 env의 잘못된 문자.
Best-effort, 보안 경계가 아님
allowed_commands는 사고에 대한 가드레일이지 보안 경계가 아니에요. 검증은 첫 토큰만 확인하고, python, git, uv, make 같은 allowlisted 명령은 임의의 프로세스를 생성할 수 있어요. allowlist를 우회하고 싶은 모델은 그럴 수 있어요. 신뢰할 수 없는 작업에는 ModalSandbox나 컨테이너 같은 OS 수준 격리 안에서 에이전트를 실행하세요.
Limit files written by commands
Shell(max_file_bytes=10_000_000)을 설정해 run_command와 start_command가 쓰는 각 일반 파일의 크기를 제한해요. 리다이렉트된 출력과 백그라운드 stdout/stderr 로그를 포함해요. None(기본값)은 한도를 추가하지 않아요. 이것은 반환된 도구 결과를 제한하는 max_output_chars와 별개예요.
양의 정수는 셸을 실행하기 전에 자식 런처에서 POSIX RLIMIT_FSIZE로 적용돼요. 후손이 상속하고, harness 부모의 한도는 변하지 않아요. 더 낮은 상속 하드 한도가 우선해요. 지원되지 않는 플랫폼, persist_cwd=True, tools=['shell'](영속 도구)은 설정을 무시하는 대신 툴셋이 구성될 때 ValueError를 발생시켜요. 작업 디렉터리 영속은 자식이 쓴 캡처 파일을 사용하며, 그것도 한도에 적용될 거예요.
파일 크기 신호로 죽은 프로세스는 진단된 도구 결과를 얻고, 다른 0이 아닌 종료는 구성된 한도를 컨텍스트로 포함해요. 프로그램이 쓰기 오류를 잡고 자체 종료 코드를 선택할 수 있으니까요. 에이전트는 출력을 줄이고 재시도할 수 있어요. 백그라운드 실패는 check_command나 stop_command에 나타나요. 오류를 처리하고 성공적으로 종료하는 명령은 종료 상태에서 진단될 수 없어요. 결과는 여전히 max_output_chars를 준수해요.
이것은 디스크 할당량이나 샌드박스가 아닌 파일별 경계예요. 부분 파일을 제거하거나, 기존 파일을 줄이거나, 더 작은 파일을 많이 만드는 것을 방지하지 않아요. 총체적 디스크 보호와 신뢰할 수 없는 명령에는 파일시스템 할당량이나 OS 격리를 사용하세요. 추가 텔레메트리 스팬은 방출되지 않아요. 기존 도구 호출 결과가 실패와 한도 컨텍스트를 지녀요.
Environment control
기본적으로 생성된 명령은 에이전트 프로세스의 전체 환경을 상속해요. LLM API 키, 토큰, 또는 다른 비밀을 보유한 샌드박스에서는 모델이 쓴 명령이 그것을 읽을 수 있어요. 두 필드가 하위 프로세스가 보는 것을 제어해요:
Field
Effect
env
하위 프로세스의 자체 환경에 대해 상속을 대체하는 명시적 환경.
denied_env_patterns
기본 환경에서 제거되는 변수 이름의 glob 패턴(fnmatch). denied_commands를 반영.
env는 상속된 변수가 하위 프로세스의 자체 환경에 나타나는 것을 방지해요(PATH와 명령이 필요로 하는 다른 모든 것을 공급). denied_env_patterns는 상속된 환경 위의 denylist예요 — 소수의 알려진 민감한 이름만 버리면 되고 가볍게 구성할 수 있어요. 둘은 결합돼요. 둘 다 설정되면 패턴이 명시적 env도 필터링해요. 둘 다 설정하지 않으면 상속 전부를 보존하는 기본이 유지돼요.
import os
from pydantic_ai_harness import LLM_API_KEY_ENV_PATTERNS, Shell
# Strip provider credentials from the inherited environment.
Shell(cwd='./repo', denied_env_patterns=LLM_API_KEY_ENV_PATTERNS)
# Or hand the subprocess a fixed environment, inheriting nothing.
Shell(cwd='./repo', env={'PATH': os.environ['PATH'], 'HOME': os.environ['HOME']})
LLM_API_KEY_ENV_PATTERNS는 일반적인 프로바이더 접두사(ANTHROPIC_*, GATEWAY_*, GEMINI_*, GOOGLE_*, OPENAI_*, OPENROUTER_*)와 PYDANTIC_AI_GATEWAY_API_KEY를 다뤄요. LLM 자격 증명만 대상으로 해요 — 다른 호스트 비밀(LOGFIRE_TOKEN, GitHub 토큰, 클라우드 자격 증명)은 다루지 않고, 접두사가 거칠어서 GOOGLE_*는 GOOGLE_APPLICATION_CREDENTIALS 같은 비자격 증명 변수도 제거해요. 시작점으로 취급하고 자체 패턴을 추가하세요. 기본값이 아니에요. 환경 변수를 조용히 제거하면 상속된 자격 증명에 의존하는 에이전트가 깨질 수 있으므로 옵트인이에요.
env는 스폰 시 강제되며 실행 중인 프로세스에 사후 필터로 적용되지 않아요. 하위 프로세스는 정확히 해석된 환경(자체 env, 그 denied_env_patterns가 제거한 것 뺀)으로 시작해요. 어느 제어도 보안 경계가 아니에요. 같은 OS 정체성 아래 실행되는 명령은 Linux procfs 같은 시스템 인터페이스와 다른 호스트 파일을 통해 부모 프로세스의 환경을 여전히 읽을 수 있어요. 명령이 신뢰할 수 없을 때는 OS 수준 격리를 사용하세요. 반대편은, PATH나 HOME을 제거할 만큼 넓은 패턴이나 그것을 생략하는 env가 명령 해석을 깨뜨릴 수 있다는 것이에요. 일부 시스템에서 셸의 내장 기본 PATH로 외부 명령이 여전히 실행될 수 있지만, 그것에 의존하지 마세요 — 환경을 대체할 때 PATH를 명시적으로 설정하세요.
Background processes
start_command는 stdout/stderr를 임시 파일에 쓰고 짧은 ID를 반환해요. 폴링에는 check_command(command_id)를, 종료와 최종 출력 수집에는 stop_command(command_id)를 사용하세요. 프로세스는 자체 세션(start_new_session)에서 시작되어 전체 프로세스 그룹에 신호를 보낼 수 있어요 — SIGTERM, 유예 기간 후 SIGKILL로 상향.
실행 종료 시 툴셋의 정리가 여전히 실행 중인 모든 백그라운드 프로세스를 종료하고 임시 파일을 삭제해요. 에이전트 런타임은 AsyncExitStack으로 툴셋에 들어가므로, 이 정리는 실행이 성공하든 예외를 발생시키든 실행돼요 — stop_command를 잊은 에이전트는 프로세스를 누출하지 않아요.
from pydantic_ai import Agent
from pydantic_ai_harness import Shell
agent = Agent(
'anthropic:claude-sonnet-4-6',
capabilities=[Shell(cwd='./app', allowed_commands=['npm', 'curl'])],
)
result = agent.run_sync(
'Start the dev server with `npm run dev`, wait for it to boot, '
'then curl http://localhost:3000/health and report the status.'
)
print(result.output)
Persistent commands
위 네 도구는 실행 범위예요. 그 프로세스는 실행과 함께 죽어요. 대신 영속 도구를 등록하려면 tools에서 shell을 이름지으세요:
from pydantic_ai_harness import Shell
Shell(cwd='./repo', tools=['shell'])
shell(command, mode='foreground', timeout=None)은 명령을 자체 세션에서 시작된 작은 감독 프로세스에 넘겨요. 감독자는 명령의 결합된 stdout과 stderr를 출력 로그에 추가하고, JSON 상태 파일({"pid": ..., "exit_code": ...}, 명령이 종료될 때까지 exit_code는 null)을 게시하고, 명령을 수확해요. 도구는 감독자의 PID와 두 경로를 반환하므로, 모델은 다른 도구로 진행 상황을 읽고 POSIX에서는 kill -- -PID, Windows에서는 taskkill /PID <PID> /T /F로 프로세스를 멈춰요(프로세스 그룹 또는 트리, 결과가 올바른 것의 이름을 지어줌). Foreground는 종료 상태를 위해 최대 timeout초(기본 default_timeout, 최대 MAX_FOREGROUND_WAIT 270)를 기다리고, 명령이 여전히 실행 중이어도 로그의 마지막 16,000바이트 다음 핸들을 반환해요. Background는 핸들을 즉시 반환해요. 핸들은 마지막에 와서, 과도한 결과의 꼬리를 유지하는 max_output_chars가 그것을 버릴 수 없어요. 270초 상한은 도구 호출이 일반적인 프로바이더 요청 타임아웃보다 짧게 유지하므로, 긴 빌드나 테스트 실행이 대화를 지연시키지 않아요. 모델은 핸들을 얻고, 다른 작업을 하고, 상태 파일을 폴링해요.
명령은 에이전트 실행, 이벤트 루프, (시작한 후에는) 호출 인터프리터를 오래 살아남으므로, 모델이 시작한 서버가 계속 서빙해요. 명령이 끝났을 때 에이전트를 깨우는 것은 없어요. 모델이 폴링해요. 취소된 foreground 호출(예: 실행 취소)은 핸들을 돌려줄 수 없으므로, 감독자의 전체 세션을 죽이고 로그 디렉터리를 제거해요. 돌려받은 로그 디렉터리는 절대 회전되거나 삭제되지 않아요. 호출자가 정리를 소유하고, 장황한 명령은 자체 출력을 제한해야 해요. 상태를 게시하지 않고 종료하는 감독자(예: 깨진 인터프리터)는 로그 디렉터리를 이름짓는 재시도로 표면화돼요.
allowed_commands, denied_commands, denied_operators, allow_interactive, env, denied_env_patterns는 shell에 run_command와 정확히 같게 적용돼요. persist_cwd는 그렇지 않아요. 모든 shell 명령은 run_command가 추적한 것이 무엇이든 구성된 cwd에서 시작해요. 명령과 로그는 호스트 로컬이며, 영속 워크플로우 활동이 아니고 재생 안전하지 않아요.
각 shell 호출은 shell 네임스페이스에서 진행 이벤트를 방출하므로, UI가 도구 결과를 파싱하지 않고 출력이 도착할 때 보여줄 수 있어요:
Event
Dispatch
Payload
CommandStartedEvent
stream
command, pid
CommandOutputEvent
stream
text: 결합 로그의 청크, 점진적으로 디코딩
CommandFinishedEvent
stream
pid, output_path, status_path, exit_code, truncated, total_lines
출력 이벤트는 foreground 호출이 기다리는 동안 방출돼요. 호출당 로그의 최대 처음 16,000바이트, 최대 4,096바이트 청크로, 50ms마다 폴링. background 호출은 시작과 완료 이벤트만 방출해요. finished는 도구가 기다림을 멈췄다는 뜻이지 명령이 종료됐다는 뜻이 아니에요. 상태가 게시되지 않는 동안 exit_code는 None이고, truncated는 로그가 이벤트가 보여준 것보다 더 많이 담았다고 말해요. total_lines는 최대 1MiB 로그의 논리 줄을 세고, 더 큰 로그(스캔되지 않음)는 None이에요. 취소된 호출은 완료 이벤트를 방출하지 않을 수 있어요. 이벤트는 명령 텍스트와 명령 출력을 지니므로 렌더링할 때 신뢰할 수 없는 것으로 취급하세요. 텔레메트리 스팬을 추가하지 않아요. core가 이미 도구 호출을 추적해요.
Working directory
기본적으로 각 명령은 cwd에서 실행되고 cd는 지속 효과가 없어요. persist_cwd=True를 설정하면 cd가 호출들 사이에 끈적해져요. 각 명령은 감싸져서, 실행된 후 최종 작업 디렉터리가 개인 임시 파일에 기록되고, 그 디렉터리가 후속 호출로 옮겨져요. 경로는 명령이 0으로 종료될 때만 갱신되고, 기록은 대역외(out-of-band, stdout이 아님)로 쓰여지므로 명령 출력이 추적된 디렉터리를 절대 위조할 수 없어요.
from pydantic_ai import Agent
from pydantic_ai_harness import Shell
agent = Agent(
'anthropic:claude-sonnet-4-6',
capabilities=[Shell(cwd='.', persist_cwd=True, allowed_commands=['cd', 'ls', 'pwd'])],
)
각 실행은 새 툴셋 인스턴스를 얻으므로, 추적된 디렉터리와 백그라운드 프로세스는 동시 실행 사이에 격리되고 항상 구성된 cwd에서 다시 시작해요.
Configuration
기본값이 있는 Shell의 모든 필드:
from pydantic_ai_harness import Shell
Shell(
cwd='.', # str | Path -- working directory
allowed_commands=[], # allowlist (mutually exclusive with denied)
denied_commands=[...], # denylist (defaults to destructive commands)
denied_operators=[], # blocked shell operators
default_timeout=30.0, # seconds, per run_command
max_output_chars=50_000, # output cap returned to the model
max_file_bytes=None, # POSIX per-file size limit (None = no added limit)
persist_cwd=False, # make cd sticky across calls
allow_interactive=False, # allow TTY-style commands
env=None, # explicit env, replacing inheritance (None = inherit)
denied_env_patterns=[], # glob patterns stripped from the env
tools=RUN_SCOPED_TOOL_NAMES, # which tools to register ('shell' for persistent commands)
)
tools=['shell']에서는 default_timeout이 foreground 대기이기도 하며 0보다 크고 최대 270초여야 해요. 그것은 구성 시 확인돼요.
Agent spec (YAML/JSON)
Shell은 Pydantic AI의 agent spec과 함께 동작하므로, Python 대신 구성 파일에서 선언할 수 있어요:
# agent.yaml
model: anthropic:claude-sonnet-4-6
capabilities:
- Shell:
cwd: ./workspace
allowed_commands: ['ls', 'cat', 'rg', 'pytest']
from pydantic_ai import Agent
from pydantic_ai_harness import Shell
agent = Agent.from_file('agent.yaml', custom_capability_types=[Shell])
spec 로더가 Shell을 인스턴스화하는 방법을 알도록 custom_capability_types를 전달하세요.
Further reading
API reference
Shell
Bases: AbstractCapability[AgentDepsT]
에이전트용 셸 명령 실행.
명령은 cwd에 뿌리를 둔 하위 프로세스에서 실행돼요. allowed_commands 또는 denied_commands를 사용해 에이전트가 무엇을 호출할 수 있는지 제어하세요.
Attributes
cwd
명령 실행용 작업 디렉터리.
Type: str | Path Default: '.'
allowed_commands
비어 있지 않으면 이 명령 이름만 실행될 수 있어요(allowlist).
Type: Sequence[str] Default: field(default_factory=(list[str]))
denied_commands
이 명령 이름은 항상 거부돼요(denylist).
기본적으로 파괴적 명령(rm, dd, shutdown 등)을 차단. 비활성화하려면 빈 목록으로 설정.
Type: Sequence[str] Default: _DEFAULT_DENIED_COMMANDS
denied_operators
차단되는 셸 연산자(제한 모드에서 예: '>', '>>', '|').
Type: Sequence[str] Default: field(default_factory=(list[str]))
default_timeout
명령 실행용 기본 타임아웃(초).
Type: float Default: 30.0
max_output_chars
모델에 반환되는 출력의 최대 문자 수. 양수여야 해요.
Type: int Default: 50000
max_file_bytes
실행 범위 자식용 선택적 POSIX 파일별 크기 한도. 총 디스크 사용량이 아님.
양수여야 해요. 지원되지 않는 플랫폼, persist_cwd, 영속 shell 도구는 거부돼요. 부모는 변하지 않고, 더 낮은 상속 하드 한도가 여전히 적용돼요.
Type: int | None Default: field(default=None, kw_only=True)
persist_cwd
True이면 cd 명령을 추적하고 후속 호출의 작업 디렉터리를 조정.
Type: bool Default: False
allow_interactive
True이면 대화형 명령(vi, nano, ssh 등)을 허용. 기본적으로 차단.
Type: bool Default: False
env
생성된 하위 프로세스용 명시적 환경. 상속 대체.
None(기본값)이면 하위 프로세스가 부모 환경을 상속. 고정 매핑으로 설정하면 하위 프로세스를 자체 환경에서 정확히 이 변수들로 시작. 이것은 보안 경계가 아니에요. 같은 OS 사용자로 실행되는 명령은 Linux procfs 같은 시스템 인터페이스를 통해 부모 프로세스에서 비밀을 읽을 수 있어요. 신뢰할 수 없는 명령에는 OS 수준 격리를 사용하세요.
Type: Mapping[str, str] | None Default: None
denied_env_patterns
스폰 전에 제거할 환경 변수 이름의 glob 패턴.
denied_* 명명 관례를 따르지만 glob(fnmatch, 예: OPENAI_*)으로 매칭해요. env 비밀은 접두사로 모이니까 — 실행 파일 이름을 정확히 매칭하는 denied_commands와 달리. 패턴과 일치하는 이름은 기본 환경에서 제거되고, 둘 다 설정되면 env 위에 적용되므로 패턴이 명시적 env도 필터링해요. 바로 사용 가능한 프로바이더 자격 증명 denylist는 LLM_API_KEY_ENV_PATTERNS를 참고하세요.
Type: Sequence[str] Default: field(default_factory=(list[str]))
tools
SHELL_TOOL_NAMES에서 등록할 도구.
기본값은 실행 범위 계열이에요. run_command, start_command, check_command, stop_command, 그 프로세스는 실행이 끝나면 죽어요. 대신 영속 도구를 등록하려면 shell을 이름지으세요. 그 명령은 실행을 오래 살고, foreground 호출은 여전히 실행 중인 프로세스의 핸들을 반환하기 전에 최대 default_timeout초(270으로 상한)를 기다리고, 모델은 다른 도구로 반환된 로그와 상태 파일을 읽어요. persist_cwd는 shell에 적용되지 않아요. 각 명령은 cwd에서 시작.
Type: Sequence[str] Default: RUN_SCOPED_TOOL_NAMES
Methods
post_init
def __post_init__() -> None
선택된 정책에 따라 내장 denylist를 해석.
Returns
get_toolset
def get_toolset() -> ShellToolset[AgentDepsT]
셸 툴셋을 구축하고 반환.
Returns
ShellToolset[AgentDepsT]