본문 바로가기

개발/AI

Spring AI로 Pi 스타일 에이전트 하네스 만들기 (9) - write/edit 붙이기

이번 글의 목적

이번 단계에서는 처음으로 파일을 변경하는 tool을 붙입니다.

write/edit은 기본 비활성화
명시적으로 켰을 때만 사용
모든 변경은 기존 policy와 실행 계층을 통과

 

목표는 writeedit을 기존 workspace safety, ToolPolicy, ToolExecutionService, ToolRegistry 위에 얹는 것입니다.

왜 write/edit은 늦게 붙일까

파일을 읽는 것과 파일을 바꾸는 것은 위험도가 다릅니다.

workspace safety
-> read-only tools
-> policy
-> Spring AI adapter
-> AgentRuntime
-> write/edit

 

이 순서 덕분에 write/edit도 기존 안전장치 위에서 동작할 수 있습니다.

M9에서 새로 중요한 것은 파일을 바꾸는 기능 자체보다, 파일을 바꿀 수 있는 조건을 좁게 유지하는 것입니다.

write tool

write는 새 파일을 만들거나 전체 파일을 덮어쓸 수 있습니다.

처음에는 덮어쓰기를 기본 차단하는 편이 안전합니다.

파일이 없으면 생성 허용
파일이 있으면 overwrite=false일 때 차단
overwrite=true일 때만 허용

 

입력은 단순합니다.

{
  "path": "docs/example.md",
  "content": "hello",
  "overwrite": false
}

 

overwrite는 선택 값이고 기본값은 false입니다.

또 하나 중요한 제한이 있습니다.

parent directory가 없으면 실패합니다.

write가 자동으로 directory를 만들지 않습니다.

자동 directory 생성을 허용하면 작은 오타가 새 디렉터리와 파일 생성으로 이어질 수 있습니다.

M9에서는 그런 편의보다 명확한 실패를 택합니다.

반환 결과는 사람이 읽을 수 있는 짧은 메시지와 metadata로 남깁니다.

{
  "path": "docs/example.md",
  "created": true,
  "overwritten": false,
  "bytesWritten": 5
}

edit tool

edit은 더 조심해야 합니다.

처음에는 복잡한 patch engine을 만들지 않고, 단순한 search/replace 방식으로 시작합니다.

target file
old text
new text

 

예를 들어:

{
  "path": "README.md",
  "oldText": "Spring AI Agent",
  "newText": "Spring AI Coding Agent"
}

 

핵심은 oldText가 정확히 한 번만 매칭될 때만 수정한다는 점입니다.

0번 발견되면 실패
2번 이상 발견되면 실패
정확히 1번 발견될 때만 수정

 

이 원칙은 매우 중요합니다.

애매한 수정은 하지 않는다.

 

oldText가 비어 있는 경우도 차단합니다.

빈 문자열은 모든 위치에 매칭될 수 있기 때문에 안전한 edit 조건으로 볼 수 없습니다.

edit은 UTF-8 text file만 대상으로 합니다.

binary file이거나 너무 큰 파일이면 실패합니다.

allowWrite로 기본 비활성화하기

writeedit은 모두 ToolSafetyLevel.WRITE입니다.

따라서 기본 옵션에서는 활성화되지 않습니다.

allowWrite=false -> write/edit 비활성화
allowWrite=true  -> write/edit 활성화

 

이 처리는 두 계층에서 확인됩니다.

먼저 ToolRegistryAgentRunOptions를 보고 model에 노출할 tool 목록을 고릅니다.

allowWrite=false이면 write, edit은 enabled tool 목록에 들어가지 않습니다.

그리고 ToolExecutionService는 실제 실행 직전에 DefaultToolPolicy를 통과시킵니다.

어떤 경로로든 WRITE tool이 실행 요청까지 들어오더라도, allowWrite=false이면 policy가 실행 전 거부합니다.

즉 M9의 write/edit은 단순히 UI나 prompt에서만 숨겨지는 것이 아닙니다.

registry와 execution policy 양쪽에서 닫혀 있습니다.

기존 안전장치를 재사용한다

M4, M5에서 만든 안전장치를 그대로 통과합니다.

SafePathResolver
SecretFileGuard
WorkspaceProperties
TextFileReader
ToolOutputLimiter

 

write는 workspace 밖 경로, path traversal, secret file 경로, 없는 parent directory를 차단합니다.

edit은 workspace 밖 경로, path traversal, secret file 경로를 차단하고, 대상 파일이 없거나 regular file이 아니면 실패합니다. 또 기존 text file 읽기 규칙을 따라 large/binary file을 거부합니다.

새 tool을 추가했지만, 안전 판단이 여기저기 흩어지지 않는 것이 중요합니다.

변경 결과 요약

write/edit 결과에는 변경 결과를 검토할 수 있는 정보를 남깁니다.

이번 단계에서는 별도의 diff engine을 만들지 않고, 짧은 결과 메시지와 metadata를 반환합니다.

write 결과 metadata는 다음 값을 담습니다.

{
  "path": "docs/example.md",
  "bytesWritten": 5,
  "created": true,
  "overwritten": false
}

 

edit 결과 metadata는 다음 값을 담습니다.

{
  "path": "README.md",
  "replacements": 1,
  "bytesWritten": 1280
}

 

이 정도만 있어도 AgentRuntime의 tool event와 session history에서 어떤 파일이 변경되었는지 확인할 수 있습니다.

diff 반환은 이후 단계에서 붙일 수 있습니다.

M9에서는 기존 구조를 크게 흔들지 않고, 안전한 write/edit 수직 슬라이스를 완성하는 데 집중합니다.

실제 패키지 구조

M9에서 추가된 중심 클래스는 다음과 같습니다.

tool/builtin/
  WriteTool.java
  EditTool.java

 

둘 다 Spring @Component입니다.

그래서 별도 등록 코드 없이 기존 ToolRegistry 생성자에 자연스럽게 들어갑니다.

ToolRegistry는 등록된 tool의 ToolSafetyLevelAgentRunOptions를 보고 enabled tool 목록을 만듭니다.

READ_ONLY -> enabledByDefault
WRITE     -> options.allowWrite()
BASH      -> options.allowBash()

 

M9는 이 구조에 WRITE tool을 추가한 것입니다.

bash는 아직 만들지 않습니다.

테스트에서 확인한 것

M9 테스트는 tool 자체와 ToolExecutionService 경유 실행을 나눠 확인합니다.

WriteTool에서는 다음을 확인합니다.

- 새 파일 생성 성공
- 기존 파일 overwrite=false에서 실패
- 기존 파일 overwrite=true에서 성공
- workspace 밖 경로 거부
- secret file 경로 거부
- missing parent directory 실패
- ToolExecutionService 경유 시 allowWrite=false에서 실행 전 거부
- ToolExecutionService 경유 시 allowWrite=true에서 실행 성공

 

EditTool에서는 다음을 확인합니다.

- oldText 정확히 1회 매칭 시 수정 성공
- oldText 미매칭 실패
- oldText 다중 매칭 실패
- workspace 밖 경로 거부
- secret file 경로 거부
- binary/large file 실패
- ToolExecutionService 경유 시 allowWrite=false에서 실행 전 거부
- ToolExecutionService 경유 시 allowWrite=true에서 실행 성공

 

이 테스트들이 중요한 이유는 간단합니다.

write/edit은 정상 동작보다 실패 조건이 더 중요합니다.

좋은 write/edit tool은 바꿀 수 있을 때보다 바꾸면 안 될 때를 더 명확히 알아야 합니다.

마무리

M9는 Agent에게 처음으로 손을 주는 단계입니다.

이제 변경 조건이 명확할 때만 움직이고, 변경 결과를 사람이 검토할 수 있게 남길 수 있습니다.

 

[codex] Add write and edit tools by dd3ok · Pull Request #12 · dd3ok/pi-spring-ai