하네스 코드에서 규칙으로 막는다: 보호 경로 쓰기 금지, 읽지 않은 기존 파일 덮어쓰기 금지, 위험 명령은 사람 승인. 거부는 예외가 아니라 BLOCKED: 이유 문자열로 돌려줘서 모델이 다른 길을 찾게 한다.
비용
정규식 기반 탐지는 우회될 수 있다. 진짜 격리(컨테이너, 권한)는 별도.
할 일 (exercise.py)
Guard.check 세 규칙
python run_tests.py 7 -v
생각해볼 것
DANGEROUS 패턴을 우회하는 명령을 떠올려 보세요. 무엇으로 보완할까요?
부록 C(플랜 모드)는 이 아이디어를 '상태'로 확장합니다.
2 따라 해보기
비유 → 짧은 코드 → 실제 출력. 아래 출력은 미리 실행해 둔 결과이고, ▶ 실행을 누르면 브라우저 안 파이썬으로 다시 돌립니다(첫 실행 때 약 10MB 내려받음).
예제 1 확신에 찬 사고 3종 세트
모델은 악의가 없어도 "설정 정리할게요" 하며 .env 를 덮어쓰거나, 읽지도 않은 파일을 추측으로 통째로 다시 씁니다. 프롬프트의 "조심해"는 부탁이지만, 가드레일은 코드로 된 규칙입니다.
import os
import tempfile
import solution as lab
os.chdir(tempfile.mkdtemp())
open("발주목록.csv", "w").write("PO-001,볼트,100\nPO-002,너트,200\n")
guard = lab.Guard(approve=lambda cmd: print(f" 🙋 사람에게 확인 요청: {cmd} → 거절") or False)
run = guard.executor()
attempts = [
("write_file", {"path": ".env", "content": "API_KEY="}),
("write_file", {"path": "발주목록.csv", "content": "PO-001,볼트,100\n"}),
("run_command", {"command": "rm -rf ./*"}),
]
for name, args in attempts:
print(f"{name}({list(args.values())[0]}) → {run(name, args)}")
print("\n발주목록.csv 는 무사합니다:", open("발주목록.csv").read().count("\n"), "줄")
실행 결과
write_file(.env) → BLOCKED: .env 는 보호된 경로입니다.
write_file(발주목록.csv) → BLOCKED: 발주목록.csv 는 이미 존재합니다. 덮어쓰기 전에 read_file 로 먼저 읽으세요.
🙋 사람에게 확인 요청: rm -rf ./* → 거절
run_command(rm -rf ./*) → BLOCKED: 사용자가 위험한 명령 실행을 거부했습니다.
발주목록.csv 는 무사합니다: 2 줄
💡 바꿔 보기: approve=lambda c: True 로 바꾸면 세 번째 명령은 통과합니다(실제로 지워지니 빈 폴더에서만!).
예제 2 거부 이유를 읽고 스스로 고치는 모델
가드레일이 예외로 하네스를 멈추면 작업이 끝나 버립니다. 대신 BLOCKED: 이유 를 돌려주면, 모델이 읽고 올바른 순서(읽기 → 쓰기) 로 다시 시도합니다.
볼트 단가만 130원으로 고쳤습니다. 너트는 그대로입니다.
[0] user "볼트 단가 130원으로 올려줘"
[1] assistant tool_use[w1] write_file({"path": "단가표.txt", "content": "볼트 130원\n"})
[2] user tool_result[w1] "BLOCKED: 단가표.txt 는 이미 존재합니다. 덮어쓰기 전에 re…"
[3] assistant text "먼저 읽어야 하는군요."
tool_use[r1] read_file({"path": "단가표.txt"})
[4] user tool_result[r1] "볼트 120원⏎너트 80원⏎"
[5] assistant tool_use[w2] write_file({"path": "단가표.txt", "content": "볼트 130원\n너트…)
[6] user tool_result[w2] "wrote 15 chars to 단가표.txt"
[7] assistant text "볼트 단가만 130원으로 고쳤습니다. 너트는 그대로입니다."
최종 파일: 볼트 130원 / 너트 80원 /
3 확인 퀴즈
Q1 '읽지 않은 기존 파일 덮어쓰기 금지' 규칙이 막는 사고는?
모델은 파일 내용을 추측으로 '재작성'하는 경우가 많습니다.
Q2 정규식 기반 위험 명령 탐지의 한계는?
진짜 격리(컨테이너, 권한)와 함께 써야 합니다.
4 실습: exercise.py 채우기
TODO 표시 부분을 채우고 채점하세요. 입력 내용은 이 기기에 자동 저장됩니다.
5 정답과 테스트
정답 코드 보기 (solution.py)
#!/usr/bin/env python3
"""Lab 7 — 반사신경과 가드레일: 루프는 언젠가 멍청한 짓을 한다.
프롬프트에 "조심해"라고 쓰는 건 부탁일 뿐입니다. 가드레일은 하네스 코드 안의 '규칙'이라
모델이 아무리 확신해도 통과할 수 없습니다. 거부할 땐 예외 대신 이유를 문자열로 돌려줘서
모델이 읽고 다른 길을 찾게 합니다.
1. 보호 경로 : .env, .git/, .ssh/ 등에는 쓰기 금지
2. 읽고 나서 쓰기: 이미 있는 파일을 이번 세션에 읽지 않고 덮어쓰기 금지
3. 승인 게이트 : 위험한 명령은 사람에게 물어보고 실행
"""
import os
import re
import sys
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..")))
from common.toolbox import execute_tool # noqa: E402
PROTECTED = (".env", ".git/", ".ssh/", "id_rsa")
DANGEROUS = [r"\brm\s+-[a-zA-Z]*r[a-zA-Z]*f", r"\brm\s+-[a-zA-Z]*f[a-zA-Z]*r",
r"git\s+push\s+.*--force", r"curl[^|]*\|\s*(ba)?sh", r"\bmkfs\b",
r"\bdd\s+if="]
def is_protected(path):
p = path.replace("\\", "/")
return any(mark in p for mark in PROTECTED)
def is_dangerous(command):
return any(re.search(pat, command) for pat in DANGEROUS)
class Guard:
def __init__(self, approve=None):
"""approve(command) -> bool : 사람에게 묻는 함수. 기본은 터미널 입력."""
self.approve = approve or (lambda cmd: input(f"\n⚠️ 실행할까요? {cmd} [y/N] ").lower() == "y")
self.read_paths = set()
def check(self, name, args):
"""허용이면 None, 거부면 "BLOCKED: <이유>" 문자열을 반환."""
# >>> TODO: 세 규칙을 구현하세요
# - write_file: 보호 경로면 BLOCKED
# - write_file: 파일이 이미 존재하는데 self.read_paths 에 없으면 BLOCKED (먼저 read_file 하라고 안내)
# - run_command: 위험하면 self.approve(command) 가 False 일 때 BLOCKED
if name == "write_file":
path = os.path.abspath(args.get("path", ""))
if is_protected(args.get("path", "")) or is_protected(path):
return f"BLOCKED: {args.get('path')} 는 보호된 경로입니다."
if os.path.exists(path) and path not in self.read_paths:
return (f"BLOCKED: {args.get('path')} 는 이미 존재합니다. "
"덮어쓰기 전에 read_file 로 먼저 읽으세요.")
if name == "run_command" and is_dangerous(args.get("command", "")):
if not self.approve(args["command"]):
return "BLOCKED: 사용자가 위험한 명령 실행을 거부했습니다."
return None
# <<< TODO
def executor(self, base_execute=execute_tool):
def execute(name, args):
verdict = self.check(name, args)
if verdict:
return verdict
result = base_execute(name, args)
if name == "read_file" and not str(result).startswith("ERROR"):
self.read_paths.add(os.path.abspath(args["path"]))
return result
return execute
if __name__ == "__main__":
g = Guard(approve=lambda c: False)
ex = g.executor()
print(ex("write_file", {"path": ".env", "content": "KEY=1"}))
print(ex("run_command", {"command": "rm -rf /tmp/whatever"}))
채점 테스트 보기 (test_lab.py)
import os
import sys
import tempfile
import unittest
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..")))
from common.labkit import load_target # noqa: E402
lab = load_target(__file__)
class Lab7(unittest.TestCase):
def setUp(self):
self.tmp = tempfile.TemporaryDirectory()
self.d = self.tmp.name
def tearDown(self):
self.tmp.cleanup()
def test_protected_path(self):
g = lab.Guard(approve=lambda c: True)
out = g.executor()("write_file", {"path": os.path.join(self.d, ".env"), "content": "x"})
self.assertTrue(out.startswith("BLOCKED"))
self.assertFalse(os.path.exists(os.path.join(self.d, ".env")))
def test_new_file_ok(self):
g = lab.Guard(approve=lambda c: True)
p = os.path.join(self.d, "new.txt")
self.assertIsNone(g.check("write_file", {"path": p, "content": "x"}))
def test_read_before_write(self):
p = os.path.join(self.d, "exists.txt")
with open(p, "w") as f:
f.write("old")
g = lab.Guard(approve=lambda c: True)
ex = g.executor()
self.assertTrue(ex("write_file", {"path": p, "content": "new"}).startswith("BLOCKED"))
self.assertEqual(open(p).read(), "old")
ex("read_file", {"path": p})
self.assertFalse(ex("write_file", {"path": p, "content": "new"}).startswith("BLOCKED"))
self.assertEqual(open(p).read(), "new")
def test_dangerous_needs_approval(self):
asked = []
g = lab.Guard(approve=lambda c: asked.append(c) or False)
out = g.check("run_command", {"command": "rm -rf build"})
self.assertTrue(out.startswith("BLOCKED"))
self.assertEqual(asked, ["rm -rf build"])
g2 = lab.Guard(approve=lambda c: True)
self.assertIsNone(g2.check("run_command", {"command": "rm -rf build"}))
def test_safe_command_not_asked(self):
g = lab.Guard(approve=lambda c: self.fail("안전한 명령엔 묻지 않아야 합니다"))
self.assertIsNone(g.check("run_command", {"command": "ls -la"}))
if __name__ == "__main__":
unittest.main()
📚 더 읽을거리
Harness Engineering 101· Isuru Wijesiri 이 워크북의 뼈대가 된 온라인 책. 장마다 ‘무엇이 깨지나 → 패치 → 비용’ 구조로 300줄짜리 하네스를 키워 갑니다.
12-Factor Agents· HumanLayer 프로덕션 에이전트 설계 원칙 12가지. ‘컨텍스트 창을 직접 소유하라’, ‘사람에게 묻는 것도 도구다’ 같은 원칙이 Lab 6·7과 맞닿아 있습니다.