콘텐츠로 이동

Bash 도구 런타임

이 문서는 에이전트 도구 호출에서 사용되는 bash 도구 런타임 경로를 설명합니다. 명령어 정규화부터 실행, 잘라내기/아티팩트, 렌더링까지 다룹니다.

또한 대화형 TUI, 출력 모드, RPC 모드, 사용자 주도 뱅(!) 셸 실행에서 동작이 달라지는 부분도 설명합니다.

coding-agent에는 두 가지 서로 다른 bash 실행 표면이 있습니다:

  1. 도구 호출 표면 (toolName: "bash"): 모델이 bash 도구를 호출할 때 사용됩니다.
    • 진입점: BashTool.execute().
  2. 사용자 뱅 명령어 표면 (대화형 입력에서 !cmd 또는 RPC bash 명령): 세션 수준 헬퍼 경로입니다.
    • 진입점: AgentSession.executeBash().

두 경로 모두 결국 비-PTY 실행을 위해 src/exec/bash-executor.tsexecuteBash()를 사용하지만, 도구 호출 경로만 정규화/차단 및 도구 렌더러 로직을 실행합니다.

도구 호출 엔드투엔드 파이프라인

섹션 제목: “도구 호출 엔드투엔드 파이프라인”

1) 입력 정규화 및 매개변수 병합

섹션 제목: “1) 입력 정규화 및 매개변수 병합”

BashTool.execute()는 먼저 normalizeBashCommand()를 통해 원시 명령어를 정규화합니다:

  • 후행 | head -n N, | head -N, | tail -n N, | tail -N을 구조화된 제한값으로 추출합니다,
  • 후행/선행 공백을 제거합니다,
  • 내부 공백은 유지합니다.

그런 다음 추출된 제한값을 명시적 도구 인수와 병합합니다:

  • 명시적 head/tail 인수가 추출된 값을 재정의합니다,
  • 추출된 값은 대체값으로만 사용됩니다.

bash-normalize.ts 주석에서는 2>&1 제거를 언급하지만, 현재 구현에서는 실제로 이를 제거하지 않습니다. 런타임 동작은 여전히 올바르지만(stdout/stderr가 이미 병합되어 있음), 정규화 동작은 주석이 시사하는 것보다 범위가 좁습니다.

2) 선택적 차단 (차단 명령어 경로)

섹션 제목: “2) 선택적 차단 (차단 명령어 경로)”

bashInterceptor.enabled가 true이면, BashTool은 설정에서 규칙을 로드하고 정규화된 명령어에 대해 checkBashInterception()을 실행합니다.

차단 동작:

  • 명령어가 차단되는 경우는 다음 조건이 모두 충족될 때만입니다:
    • 정규식 규칙이 일치하고,
    • 제안된 도구가 ctx.toolNames에 존재할 때.
  • 유효하지 않은 정규식 규칙은 조용히 건너뜁니다.
  • 차단 시, BashTool은 다음 메시지와 함께 ToolError를 던집니다:
    • Blocked: ...
    • 원본 명령어 포함.

기본 규칙 패턴(코드에 정의됨)은 일반적인 오용을 대상으로 합니다:

  • 파일 읽기 도구 (cat, head, tail, …)
  • 검색 도구 (grep, rg, …)
  • 파일 찾기 도구 (find, fd, …)
  • 인플레이스 편집기 (sed -i, perl -i, awk -i inplace)
  • 셸 리디렉션 쓰기 (echo ... > file, heredoc 리디렉션)

InterceptionResult에는 suggestedTool이 포함되어 있지만, BashTool은 현재 메시지 텍스트만 표시합니다(details에 구조화된 제안 도구 필드 없음).

3) CWD 유효성 검사 및 타임아웃 클램핑

섹션 제목: “3) CWD 유효성 검사 및 타임아웃 클램핑”

cwd는 세션 cwd(resolveToCwd) 기준으로 해석된 후, stat를 통해 유효성이 검증됩니다:

  • 경로가 없는 경우 -> ToolError("Working directory does not exist: ...")
  • 디렉터리가 아닌 경우 -> ToolError("Working directory is not a directory: ...")

타임아웃은 [1, 3600]초로 클램핑되어 밀리초로 변환됩니다.

실행 전에 도구는 잘라낸 출력 저장을 위한 아티팩트 경로/ID를 최선 노력(best-effort)으로 할당합니다.

  • 아티팩트 할당 실패는 치명적이지 않습니다(아티팩트 스필 파일 없이 실행이 계속됩니다),
  • 아티팩트 ID/경로는 잘라내기 시 전체 출력 영속을 위해 실행 경로에 전달됩니다.

BashTool은 다음 조건이 모두 참일 때만 PTY 실행을 선택합니다:

  • bash.virtualTerminal === "on"
  • PI_NO_PTY !== "1"
  • 도구 컨텍스트에 UI가 있을 때 (ctx.hasUI === true이고 ctx.ui가 설정됨)

그렇지 않으면 비대화형 executeBash()를 사용합니다.

즉, 출력 모드와 비-UI RPC/도구 컨텍스트는 항상 비-PTY를 사용합니다.

executeBash()는 다음 키로 구성된 프로세스 전역 맵에 네이티브 Shell 인스턴스를 캐시합니다:

  • 셸 경로,
  • 설정된 명령어 접두사,
  • 스냅샷 경로,
  • 직렬화된 셸 환경 변수,
  • 선택적 에이전트 세션 키.

세션 수준 실행의 경우, AgentSession.executeBash()sessionKey: this.sessionId를 전달하여 세션별로 재사용을 격리합니다.

도구 호출 경로는 sessionKey를 전달하지 않으므로, 재사용 범위는 셸 설정/스냅샷/환경 변수에 기반합니다.

각 호출마다 실행기는 설정의 셸 구성(shell, env, 선택적 prefix)을 로드합니다.

선택한 셸이 bash를 포함하는 경우, getOrCreateSnapshot() 시도합니다:

  • 스냅샷은 사용자 rc의 별칭/함수/옵션을 캡처합니다,
  • 스냅샷 생성은 최선 노력(best-effort)입니다,
  • 실패 시 스냅샷 없이 대체됩니다.

prefix가 설정된 경우, 명령어는 다음과 같이 됩니다:

<prefix> <command>

Shell.run()은 청크를 콜백으로 스트리밍합니다. 실행기는 각 청크를 OutputSink와 선택적 onChunk 콜백으로 파이프합니다.

취소:

  • 중단 신호가 발생하면 shellSession.abort(...)를 트리거합니다,
  • 네이티브 결과의 타임아웃은 cancelled: true + 주석 텍스트로 매핑됩니다,
  • 명시적 취소도 마찬가지로 cancelled: true + 주석을 반환합니다.

타임아웃/취소에 대해 실행기 내부에서는 예외가 던져지지 않습니다; 구조화된 BashResult를 반환하고 호출자가 오류 의미를 매핑하도록 합니다.

대화형 PTY 경로 (runInteractiveBashPty)

섹션 제목: “대화형 PTY 경로 (runInteractiveBashPty)”

PTY가 활성화되면, 도구는 오버레이 콘솔 컴포넌트를 열고 네이티브 PtySession을 구동하는 runInteractiveBashPty()를 실행합니다.

주요 동작:

  • xterm-headless 가상 터미널이 오버레이에서 뷰포트를 렌더링합니다,
  • 키보드 입력이 정규화됩니다(Kitty 시퀀스 및 애플리케이션 커서 모드 처리 포함),
  • 실행 중 esc를 누르면 PTY 세션이 종료됩니다,
  • 터미널 크기 변경이 PTY에 전파됩니다(session.resize(cols, rows)).

무인 실행을 위해 환경 강화 기본값이 주입됩니다:

  • 페이저 비활성화 (PAGER=cat, GIT_PAGER=cat 등),
  • 편집기 프롬프트 비활성화 (GIT_EDITOR=true, EDITOR=true, …),
  • 터미널/인증 프롬프트 축소 (GIT_TERMINAL_PROMPT=0, SSH_ASKPASS=/usr/bin/false, CI=1),
  • 비대화형 동작을 위한 패키지 관리자/도구 자동화 플래그.

PTY 출력은 정규화(CRLF/CRLF로, sanitizeText)되어 아티팩트 스필 지원을 포함하여 OutputSink에 기록됩니다.

PTY 시작/런타임 오류 시, 싱크는 PTY error: ... 라인을 수신하고 명령어는 정의되지 않은 종료 코드로 완료됩니다.

출력 처리: 스트리밍, 잘라내기, 아티팩트 스필

섹션 제목: “출력 처리: 스트리밍, 잘라내기, 아티팩트 스필”

PTY와 비-PTY 경로 모두 OutputSink를 사용합니다.

  • 메모리 내 UTF-8 안전 테일 버퍼를 유지합니다(DEFAULT_MAX_BYTES, 현재 50KB),
  • 확인된 총 바이트/라인 수를 추적합니다,
  • 아티팩트 경로가 존재하고 출력이 오버플로되면(또는 파일이 이미 활성 상태이면), 전체 스트림을 아티팩트 파일에 기록합니다,
  • 메모리 임계값이 오버플로되면, 메모리 내 버퍼를 테일로 트리밍합니다(UTF-8 경계 안전),
  • 오버플로/파일 스필이 발생하면 truncated로 표시합니다.

dump()는 다음을 반환합니다:

  • output (가능한 주석 접두사 포함),
  • truncated,
  • totalLines/totalBytes,
  • outputLines/outputBytes,
  • 아티팩트 파일이 활성인 경우 artifactId.

런타임 잘라내기는 OutputSink에서 바이트 임계값 기반입니다(기본 50KB). 이 코드 경로에서는 하드 2000라인 제한을 적용하지 않습니다.

비-PTY 실행의 경우, BashTool은 부분 업데이트를 위한 별도의 TailBuffer를 사용하고 명령어 실행 중에 onUpdate 스냅샷을 발행합니다.

PTY 실행의 경우, 실시간 렌더링은 onUpdate 텍스트 청크가 아닌 커스텀 UI 오버레이에 의해 처리됩니다.

결과 형성, 메타데이터 및 오류 매핑

섹션 제목: “결과 형성, 메타데이터 및 오류 매핑”

실행 후:

  1. cancelled 처리:
    • 중단 신호가 중단된 경우 -> ToolAbortError 던지기 (중단 의미론),
    • 그렇지 않으면 -> ToolError 던지기 (도구 실패로 처리).
  2. PTY timedOut -> ToolError 던지기.
  3. 최종 출력 텍스트에 head/tail 필터 적용 (applyHeadTail, head 먼저 그 다음 tail).
  4. 빈 출력은 (no output)이 됩니다.
  5. toolResult(...).truncationFromSummary(result, { direction: "tail" })를 통해 잘라내기 메타데이터 첨부.
  6. 종료 코드 매핑:
    • 종료 코드 없음 -> ToolError("... missing exit status")
    • 0이 아닌 종료 -> ToolError("... Command exited with code N")
    • 0 종료 -> 성공 결과.

성공 페이로드 구조:

  • content: 텍스트 출력,
  • 잘라내기 시 details.meta.truncation, 포함 내용:
    • direction, truncatedBy, 총/출력 라인+바이트 수,
    • shownRange,
    • 사용 가능한 경우 artifactId.

내장 도구는 wrapToolWithMetaNotice()로 래핑되므로, 잘라내기 알림 텍스트가 최종 텍스트 콘텐츠에 자동으로 추가됩니다(예: Full: artifact://<id>).

도구 호출 렌더러 (bashToolRenderer)

섹션 제목: “도구 호출 렌더러 (bashToolRenderer)”

bashToolRenderer는 도구 호출 메시지(toolCall / toolResult)에 사용됩니다:

  • 축소 모드에서는 시각적 라인이 잘라낸 미리보기를 보여줍니다,
  • 확장 모드에서는 현재 사용 가능한 모든 출력 텍스트를 보여줍니다,
  • 경고 라인에는 잘라내기 사유와 잘라내기 시 artifact://<id>가 포함됩니다,
  • 타임아웃 값(인수에서)이 하단 메타데이터 라인에 표시됩니다.

BashRenderContext에는 isFullOutput이 있지만, 현재 렌더러 컨텍스트 빌더는 bash 도구 결과에 대해 이를 설정하지 않습니다. 확장 뷰는 다른 호출자가 전체 아티팩트 콘텐츠를 제공하지 않는 한 결과 콘텐츠에 이미 있는 텍스트(테일/잘라낸 출력)를 사용합니다.

사용자 뱅 명령어 컴포넌트 (BashExecutionComponent)

섹션 제목: “사용자 뱅 명령어 컴포넌트 (BashExecutionComponent)”

BashExecutionComponent는 대화형 모드에서 사용자 ! 명령어를 위한 것입니다(모델 도구 호출이 아님):

  • 청크를 실시간으로 스트리밍합니다,
  • 축소 미리보기는 마지막 20개 논리 라인을 유지합니다,
  • 라인당 4000자에서 라인 클램프합니다,
  • 메타데이터가 존재할 때 잘라내기 + 아티팩트 경고를 표시합니다,
  • 취소/오류/종료 상태를 별도로 표시합니다.

이 컴포넌트는 CommandController.handleBashCommand()에 의해 연결되며 AgentSession.executeBash()에서 데이터를 받습니다.

표면진입 경로PTY 사용 가능 여부실시간 출력 UX오류 표시
대화형 도구 호출BashTool.execute예, bash.virtualTerminal=on이고 UI가 존재하며 PI_NO_PTY!=1일 때PTY 오버레이 (대화형) 또는 스트리밍 테일 업데이트도구 오류가 toolResult.isError로 변환됨
출력 모드 도구 호출BashTool.execute아니오 (UI 컨텍스트 없음)TUI 오버레이 없음; 이벤트 스트림/최종 어시스턴트 텍스트 흐름에 출력 표시동일한 도구 오류 매핑
RPC 도구 호출 (에이전트 도구)BashTool.execute보통 UI 없음 -> 비-PTY구조화된 도구 이벤트/결과동일한 도구 오류 매핑
대화형 뱅 명령어 (!)AgentSession.executeBash + BashExecutionComponent아니오 (실행기 직접 사용)전용 bash 실행 컴포넌트컨트롤러가 예외를 잡아 UI 오류 표시
RPC bash 명령어rpc-mode -> session.executeBash아니오BashResult를 직접 반환소비자가 반환된 필드를 처리
  • 인터셉터는 제안된 도구가 현재 컨텍스트에서 사용 가능한 경우에만 명령어를 차단합니다.
  • 아티팩트 할당이 실패하면 잘라내기는 여전히 발생하지만 artifact:// 역참조를 사용할 수 없습니다.
  • 셸 세션 캐시에는 이 모듈에서 명시적 퇴거(eviction)가 없습니다; 수명은 프로세스 범위입니다.
  • PTY와 비-PTY 타임아웃 표면이 다릅니다:
    • PTY는 명시적 timedOut 결과 필드를 노출합니다,
    • 비-PTY는 타임아웃을 cancelled + annotation 요약으로 매핑합니다.