14. מעקות בטיחות

"סמכו אבל תבדקו — ואז תבדקו שוב"

25 דקות קריאה
💡חדש בנושא?

למה רשימה שחורה לא מספיקה לבטיחות?

רשימה שחורה כמו `['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@dataclass
2class ToolPermission:
3 tool_name: str
4 auto_approve: bool = False
5 requires_approval: bool = False
6 denied: bool = False
7 cost_limit: float | None = None
8 
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.0
13 self.cost_cap = 5.00 # dollars
14 
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 path
37 return False
38 
ToolPermission מגדיר את המדיניות לכלי אחד. כל כלי יכול לקבל אישור אוטומטי (נתיב מהיר), לדרוש אישור אנושי, או להידחות לחלוטין.
שלב 1 מתוך 4
🧪 נסו בעצמכם
🔥 חימום ~5 min

רשמו 3 דרכים לעקוף את הרשימה השחורה מ[לולאת הסוכן](/he/s01-the-agent-loop) (['rm -rf /', 'sudo']). ואז הסבירו למה הרשאות מבוססות יכולות לא סובלות מהחורים האלה.

רמז

חשבו על: משתני סביבה ($SHELL), קידוד, שרשור פקודות (&&), ו-aliases.

🔨 בנייה ~20 min

ממשו את מערכת ה-guardrail המלאה: הוסיפו פונקציית human_approve() שמדפיסה את קריאת הכלי הממתינה ומחכה לקלט y/n. שלבו אותה בלולאת הסוכן מ[לולאת הסוכן](/he/s01-the-agent-loop).

רמז

הכניסו את בדיקת ה-guard בין ניתוח קריאת הכלי לביצוע הכלי בלולאה.

🚀 אתגר ~45 min

הוסיפו סנדבוקס בקונטיינר: עטפו את ביצוע כלי ה-bash ב-docker run --rm --network none כך שהסוכן לא יוכל לגשת לרשת או למערכת הקבצים של המארח.

רמז

הרכיבו רק את תיקיית העבודה כ-volume. השתמשו בדגל --read-only עם tmpfs לכתיבה ב-/tmp.

מצאתם טעות? דווחו ←