14. מעקות בטיחות
"סמכו אבל תבדקו — ואז תבדקו שוב"
חדש בנושא?
למה רשימה שחורה לא מספיקה לבטיחות?
רשימה שחורה כמו `['rm -rf /', 'sudo']` ניתנת לעקיפה בקלות — 'rm -rf /' עובד עם רווחים נוספים, משתני סביבה, או aliases. בטיחות אמיתית צריכה בקרות מבניות: ביצוע בסנדבוקס, הרשאות מבוססות יכולות, ומגבלות עלות.
מה זה human-in-the-loop?
דפוס שבו הסוכן עוצר לפני ביצוע פעולות מסוכנות ומבקש אישור מהמשתמש. הרתמה מיירטת את קריאת הכלי, מציגה אותה למשתמש, וממשיכה רק אם הם מאשרים.
מה זה הרשאות מבוססות יכולות?
במקום לחסום דברים רעים (רשימה שחורה), אתם מתירים במפורש רק דברים טובים (רשימה לבנה). סוכן עם יכולת 'מערכת קבצים לקריאה בלבד' יכול לקרוא כל קובץ אבל לא לכתוב. זה בטוח יותר כי פקודות לא יד��עות נדחות כברירת מחדל.
הבעיה
בלולאת הסוכן, הוספנו רשימה שחורה לכלי ה-bash:
dangerous = ["rm -rf /", "sudo", "shutdown", "reboot", "> /dev/"]
if any(d in command for d in dangerous):
return "Error: Dangerous command blocked"
זה מרגיש בטוח. זה לא. הנה שלוש דרכים לעקוף את זה בשניות:
עקיפה 1: רווחים נוספים. הרשימה השחורה בודקת "rm -rf /" אבל rm -rf / (רווחים כפולים) עוברת. וגם rm -r -f /.
עקיפה 2: הרחבת משתני סביבה. $SHELL -c "rm -rf /" מריצה את הפקודה המסוכנת בתוך sub-shell. הרשימה השחורה רואה $SHELL -c ..., לא rm -rf /.
עקיפה 3: שרשור פקודות. echo hello && su -c reboot — הרשימה השחורה לא מזהה את su. וגם לא doas reboot, pkexec reboot, או כתיבת סקריפט לדיסק והרצתו.
הפגם הבסיסי: רשימות שחורות מנסות למנות הכל רע. הקבוצה של פקודות מסוכנות היא אינסופית. אי אפשר לנצח את המשחק הזה.
הפתרון
במקום בדיקה שברירית אחת, בונים חמש שכבות הגנה. כל שכבה תופסת את מה שהקודמת פספסה:
שכבה 1: הרשאות מבוססות יכולות — רק כלים מורשים יכולים לרוץ
שכבה 2: היוריסטיקות סכנה — זיהוי דפוסים בתוך כלים מורשים
שכבה 3: Human-in-the-Loop — שאלו את המשתמש לפני פעולות מסוכנות
שכבה 4: תקרת עלויות — תקציב קשיח עוצר סוכנים פרועים
שכבה 5: סנדבוקס בקונטיינר — גם אם הכל נכשל, רדיוס הפיצוץ מוגבל
שכבה 1: הרשאות מבוססות יכולות
הרעיון המרכזי: במקום לחסום דברים רעים, להתיר במפורש רק דברים טובים.
@dataclass
class ToolPermission:
tool_name: str
auto_approve: bool = False
requires_approval: bool = False
denied: bool = False
permissions = [
ToolPermission("bash", auto_approve=True),
ToolPermission("read_file", auto_approve=True),
ToolPermission("write_file", requires_approval=True),
ToolPermission("execute_sql", denied=True),
]
כל כלי שלא ברשימת ההרשאות נדחה כברירת מחדל. זה ההיפך מרשימה שחורה — כלים לא ידועים נחסמים, לא מורשים.
שכבה 3: Human-in-the-Loop
def human_approve(tool_name: str, tool_input: dict) -> bool:
print(f"\n{'='*50}")
print(f"APPROVAL REQUIRED: {tool_name}")
print(f"Input: {json.dumps(tool_input, indent=2)}")
print(f"{'='*50}")
while True:
answer = input("Allow? [y/n]: ").strip().lower()
if answer in ("y", "yes"):
return True
if answer in ("n", "no"):
return False
שכבה 5: סנדבוקס בקונטיינר
הגנה אחרונה: גם אם הסוכן עוקף הרשאות, היוריסטיקות, אישור, ומגבלות עלות — הוא לא יכול לברוח מהקונטיינר.
def run_bash_sandboxed(command: str, workspace: str) -> str:
docker_cmd = [
"docker", "run", "--rm",
"--network", "none",
"--read-only",
"--tmpfs", "/tmp:size=100m",
"-v", f"{workspace}:/work",
"-w", "/work",
"--memory", "512m",
"--cpus", "1.0",
"python:3.12-slim",
"bash", "-c", command,
]
try:
r = subprocess.run(docker_cmd, capture_output=True, text=True, timeout=120)
out = (r.stdout + r.stderr).strip()
return out[:50000] if out else "(no output)"
except subprocess.TimeoutExpired:
return "Error: Timeout (120s)"
מה השתנה מהערכת סוכנים
| דאגה | Evals | מעקות בטיחות |
|---|---|---|
| מתי רץ | לפני פריסה (זמן בדיקה) | בזמן פריסה (runtime) |
| מה תופס | פלטים שגויים, רגרסיות | פעולות מסוכנות, חריגות עלות |
| מי מוגן | מפתחים (איכות) | משתמשים ומערכות (בטיחות) |
נקודה מרכזית
בטיחות היא לא בדיקה אחת — היא מחסנית. הרשאות יכולות דוחות כלים לא ידועים. היוריסטיקות מעלות קלטים חשודים. Human-in-the-loop תופס מה שהיוריסטיקות מפספסות. תקרות עלות מונעות הוצאה פרועה. סנדבוקס בקונטיינר מגביל את רדיוס הפיצוץ כשהכל האחר נכשל. בנו את כל חמש השכבות ברתמה שלכם לפני שתיתנו לסוכן לרוץ ללא פיקוח.
מדריך קוד אינטראקטיבי
1@dataclass2class ToolPermission:3 tool_name: str4 auto_approve: bool = False5 requires_approval: bool = False6 denied: bool = False7 cost_limit: float | None = None8 9class GuardRail:10 def __init__(self, permissions: list[ToolPermission]):11 self.perms = {p.tool_name: p for p in permissions}12 self.total_cost = 0.013 self.cost_cap = 5.00 # dollars14 15 def check(self, tool_name: str, tool_input: dict) -> str:16 perm = self.perms.get(tool_name)17 if not perm or perm.denied:18 return "DENIED"19 if self.total_cost > self.cost_cap:20 return "COST_CAP_EXCEEDED"21 if perm.requires_approval:22 return "NEEDS_APPROVAL"23 if perm.auto_approve:24 if self._is_dangerous(tool_name, tool_input):25 return "NEEDS_APPROVAL"26 return "APPROVED"27 return "NEEDS_APPROVAL"28 29 def _is_dangerous(self, tool_name: str, tool_input: dict) -> bool:30 if tool_name == "bash":31 cmd = tool_input.get("command", "")32 write_patterns = ["rm ", "mv ", ">", "chmod", "kill"]33 return any(p in cmd for p in write_patterns)34 if tool_name == "write_file":35 path = tool_input.get("path", "")36 return ".env" in path or "credentials" in path37 return False38 ToolPermission מגדיר את המדיניות לכלי אחד. כל כלי יכול לקבל אישור אוטומטי (נתיב מהיר), לדרוש אישור אנושי, או להידחות לחלוטין.רשמו 3 דרכים לעקוף את הרשימה השחורה מ[לולאת הסוכן](/he/s01-the-agent-loop) (['rm -rf /', 'sudo']). ואז הסבירו למה הרשאות מבוססות יכולות לא סובלות מהחורים האלה.
רמז
חשבו על: משתני סביבה ($SHELL), קידוד, שרשור פקודות (&&), ו-aliases.
ממשו את מערכת ה-guardrail המלאה: הוסיפו פונקציית human_approve() שמדפיסה את קריאת הכלי הממתינה ומחכה לקלט y/n. שלבו אותה בלולאת הסוכן מ[לולאת הסוכן](/he/s01-the-agent-loop).
רמז
הכניסו את בדיקת ה-guard בין ניתוח קריאת הכלי לביצוע הכלי בלולאה.
הוסיפו סנדבוקס בקונטיינר: עטפו את ביצוע כלי ה-bash ב-docker run --rm --network none כך שהסוכן לא יוכל לגשת לרשת או למערכת הקבצים של המארח.
רמז
הרכיבו רק את תיקיית העבודה כ-volume. השתמשו בדגל --read-only עם tmpfs לכתיבה ב-/tmp.