이번 글의 목적
M4에서 workspace safety를 만들었다면, 이제 처음으로 파일을 읽는 도구를 붙일 수 있습니다.
읽기만 허용한다.
쓰기, 수정, 명령 실행은 아직 금지한다.
만드는 것은 Spring AI Tool Calling 연결이 아닙니다.
모델에게 바로 도구를 넘기기 전에, 애플리케이션 내부에서 안전하게 실행할 수 있는 read-only tool 계약과 built-in tool을 먼저 만듭니다.
왜 read-only부터 시작할까
먼저 읽을 수 있어야 한다.
읽은 내용을 설명할 수 있어야 한다.
그다음 수정할 수 있어야 한다.
새 프로젝트에 들어온 신입에게 첫날부터 코드를 고치라고 하지 않습니다.
먼저 README, build file, package structure, test를 읽게 합니다.
AI Agent도 같습니다.
읽기 도구가 지켜야 할 최소 계약과 테스트를 먼저 고정합니다.
만들 도구
처음 read-only 도구는 네 개로 나눴습니다.
| 도구 | 역할 |
|---|---|
ls |
디렉터리 목록을 보여준다 |
read |
UTF-8 텍스트 파일 하나를 읽는다 |
find |
glob 패턴으로 파일을 찾는다 |
grep |
작은 텍스트 파일에서 문자열을 찾는다 |
처음부터 grep의 모든 기능을 구현할 필요는 없습니다.
작고 예측 가능한 도구가 좋습니다.
그래서 find는 glob 기반 파일 검색으로 시작하고, grep은 regex가 아니라 literal substring 검색으로 시작합니다.
Tool contract
도구를 바로 Spring AI에 붙이지 않고, 내부 계약부터 잡습니다.
핵심 타입은 다음과 같습니다.
AgentTool
ToolSpec
ToolArguments
ToolResult
ToolSafetyLevel
ToolRegistry
M5의 AgentTool은 일부러 작게 둡니다.
public interface AgentTool {
ToolSpec spec();
ToolResult execute(ToolArguments arguments);
}
아직 ToolExecutionContext, ToolPolicy, ToolExecutionService는 없습니다.
이들은 M6에서 공통 실행 정책을 붙일 때 도입합니다.
지금은 각 도구가 스스로 안전해야 합니다.
read 도구
read는 반드시 M4의 SafePathResolver를 통과해야 합니다.
AI가 path 요청
→ SafePathResolver 검증
→ SecretFileGuard 검증
→ 파일 크기 확인
→ UTF-8 텍스트 여부 확인
→ 내용 반환
도구 결과에는 파일 내용만 던지지 말고 메타데이터를 함께 주는 편이 좋습니다.
{
"text": "...",
"error": false,
"metadata": {
"bytes": 1240,
"truncated": false
}
}
이렇게 해야 나중에 모델이 결과를 더 잘 이해합니다.
중요한 점이 하나 더 있습니다.
SecretFileGuard에는 절대경로가 아니라 workspace 기준 상대경로를 넘깁니다.
workspace 상위 폴더 이름이 우연히 secrets.* 같은 패턴과 맞아도, 정상 파일 읽기가 막히면 안 되기 때문입니다.
output limit
파일이 너무 크면 전체를 모델에 보내면 안 됩니다.
그래서 출력 제한이 필요합니다.
agent:
tools:
max-tool-output-chars: 20000
파일 자체가 너무 크면 read는 실패합니다.
읽을 수 있는 파일이라도 도구 결과가 너무 길면 앞부분만 반환하고 truncated: true를 metadata에 표시합니다.
{
"text": "앞부분 일부...",
"error": false,
"metadata": {
"bytes": 42000,
"truncated": true
}
}
이것은 비용과 안정성 모두에 중요합니다.
ls 도구
ls는 프로젝트 구조를 이해하는 데 필요합니다.
반환 예시는 다음처럼 단순하게 시작합니다.
README.md
build.gradle.kts
src/
docs/
여기서도 secret entry는 제외합니다.
.env 제외
.env.* 제외
*.pem 제외
*.key 제외
.git 제외
디렉터리 항목 수에도 제한을 둡니다. 너무 큰 디렉터리를 한 번에 모델에게 넘기지 않기 위해서입니다.
find 도구
find는 파일 이름과 상대 경로를 찾는 도구입니다.
처음 구현은 glob 기반입니다.
*.java
src/**/*.java
*.java는 파일명 기준으로 매칭합니다. src/**/*.java처럼 경로가 들어간 패턴은 workspace 상대 경로 기준으로 매칭합니다.
이때 .git, build, .gradle, secret path는 결과에서 제외합니다. 검색 시작 경로 자체가 .git이나 build이면 바로 거부합니다.
작은 구현에서도 이런 테스트는 필요합니다.
예를 들어 src/**/*.java는 src/main/App.java뿐 아니라 src/App.java도 잡아야 합니다.
**/가 중간 디렉터리 0개도 의미하기 때문입니다.
grep 도구
grep은 텍스트 파일에서 문자열을 찾습니다.
처음에는 regex를 지원하지 않습니다.
query가 포함된 줄을 찾는다.
검색 대상 파일도 제한합니다.
large file은 skip
binary file은 skip
secret file은 제외
.git, build, .gradle은 제외
skip된 파일 수는 metadata에 남깁니다.
{
"metadata": {
"matches": 3,
"skippedLargeFiles": 1,
"skippedBinaryFiles": 2
}
}
여기서 조심할 점이 있습니다.
최대 매칭 수에 도달했다고 해서 workspace 탐색 자체를 바로 멈추면, 뒤쪽에 있는 large/binary file의 skip count가 누락됩니다.
그래서 grep은 더 이상 match line을 추가하지 않더라도 파일 순회는 계속하면서 skip metadata를 계산합니다.
이 단계에서 하지 않는 것
write 구현
edit 구현
bash 구현
Spring AI Tool Calling 연결
ToolPolicy 구현
ToolExecutionService 구현
읽기 도구를 만들었다고 바로 모델에게 연결하지 않습니다.
먼저 내부 도구가 안전하게 동작하고, 테스트로 경계가 고정되어야 합니다.
반드시 작성할 테스트
ls는 디렉터리 목록을 반환하고 secret entry를 숨긴다.
read는 UTF-8 텍스트 파일을 읽는다.
read는 workspace 밖 경로를 차단한다.
read는 secret file을 차단한다.
read는 large/binary file을 실패시킨다.
find는 glob으로 파일을 찾고 secret/ignored path를 제외한다.
grep은 문자열을 찾고 secret/ignored path를 제외한다.
grep은 large/binary file을 skip하고 metadata에 기록한다.
output limit을 넘으면 truncated metadata를 남긴다.
ToolRegistry는 enabled read-only tool을 조회한다.
이 테스트는 나중에 Spring AI Tool Calling을 붙일 때도 그대로 안전망이 됩니다.
마무리
M5가 끝나면 Agent는 아직 코드를 고치지 못합니다.
하지만 프로젝트를 읽고 이해할 수 있는 기반이 생깁니다.
AI Agent에게 처음 필요한 것은 손이 아니라 눈입니다.
[codex] Add read-only workspace tools by dd3ok · Pull Request #8 · dd3ok/pi-spring-ai
'개발 > AI' 카테고리의 다른 글
| Spring AI로 Pi 스타일 에이전트 하네스 만들기 (7) - Tool Calling 감싸기 (1) | 2026.07.02 |
|---|---|
| Spring AI로 Pi 스타일 에이전트 하네스 만들기 (6) - Tool 실행 정책 (0) | 2026.07.01 |
| Spring AI로 Pi 스타일 에이전트 하네스 만들기 (4) - 파일 읽기 전 안전 장치 (0) | 2026.06.29 |
| Spring AI로 Pi 스타일 에이전트 하네스 만들기 (3) - 작업 일지 기록 (0) | 2026.06.29 |
| Spring AI로 Pi 스타일 에이전트 하네스 만들기 (2) - 상태 전이를 위한 타입 정의 (0) | 2026.06.26 |