חזרה לבלוג

לגרום לאייג'נט להוכיח שזה עבד

הרצתי שינוי שלם דרך אייג'נט אחד, מקצה לקצה, וכל שלב היה חייב להוכיח את עצמו מול המערכת החיה לפני שהבא ממשיך.

claude-code agents verification security

אייג’נט שמאמת את העבודה של עצמו יכול להשלים משימה אמיתית מקצה לקצה. האימות-העצמי הזה הוא מה שהופך את האוטונומיה לאמיתית. מסרתי לאייג’נט אחד שינוי שלם ולא הנעתי אף שלב, כי בכל צעד הוא בדק את העבודה של עצמו מול האתר החי לפני שהמשיך.

הדיפלויים היו החלק הקל.

הלופ, לא פקודה בודדת

הפער בין “הטסטים עוברים” ל”עובד בפרודקשן” הוא המקום שבו חיים האירועים. אז נתתי לעבודה צורה שהאימות בנוי לתוכה:

  • ריוויו לשינוי.
  • דיפלוי לסביבת ה-dev.
  • אימות ה-dev מול האתר הרץ, בדיוק כמו שמשתמש פוגש אותו.
  • דיפלוי לפרודקשן.
  • אימות הפרודקשן באותה צורה.

שני שלבי האימות הם כל העניין. לא מוק, לא חבילת טסטים שעוברת. לטעון את הדף החי, להשתמש בו כמו משתמש, לקרוא לאייג’נט ולקבל תשובה.

לאמת זה להתחבר כמו משתמש

זאת פלטפורמת תוכן של לקוח עם ממשק של שאלות ותשובות. הבדיקה היחידה שנחשבת היא להתחבר, לשאול משהו, ולוודא שחוזרת תשובה אמיתית ומבוססת. זה אומר שהאייג’נט צריך סשן מחובר על האתר החי.

הפרודקשן משתמש ב-auth דרך לינק במייל. לסביבת ה-dev יש פלאג שמחבר אותך אוטומטית, כדי שתוכל להתנייד בלי לחכות למייל.

אז שאלתי את השאלה העצלה. אפשר להדליק את הפלאג הזה גם בפרודקשן, כדי שהאייג’נט יוכל לאמת בלי התחברות.

הוא אמר לא, והסביר לי למה.

העקיפה הייתה מחלקת לציבור מפתח-אב

הפלאג מתחזה לחשבון אדמין. תדליק אותו בפרודקשן וכל מבקר אנונימי באתר החי מאומת כאדמין מלא. כל המידע של המשתמשים, החיובים, כל פעולה מורשית, בלי סיסמה. הקוד חוסם את זה בקשיחות כך שאי אפשר להדליק את זה בפרודקשן אפילו בטעות:

const BYPASS_AUTH =
  process.env.NODE_ENV !== "production" && process.env.AUTH_BYPASS === "1";

להסיר את החסם הזה זה להחליף קליק אחד בדפדפן בחור קבוע של השתלטות על חשבונות. האייג’נט אמר בדיוק את זה, תיאר את רדיוס הנזק במילים האלה, וסירב לבנות את זה.

ואז הוא נתן לי את הגרסה הבטוחה של מה שבאמת רציתי.

סשן ממוקד הוא המקבילה הבטוחה

מה שרציתי היה סשן מחובר שהאייג’נט יכול להפעיל. הדרך הלא-בטוחה נותנת אותו לכולם. הדרך הבטוחה נותנת אותו לזהות אחת, ממוקדת ובת-חלוף.

סשן כאן הוא שני דברים. שורה בטבלת הסשנים, וקוקי מוצפן שנושא את ה-id של השורה הזאת. זרימת ה-login יוצרת את שניהם. אז האייג’נט כתב סקריפט קטן שעושה את אותו דבר אופליין, ומשתמש מחדש בקוד ההצפנה של האפליקציה עצמה במקום להמציא אותו מחדש:

  • הוא יוצר משתמש ייעודי בדרגת ההרשאה הנמוכה ביותר, ומסרב לייצר סשן לאדמין. בדיקה של פלטפורמת תוכן צריכה משתמש מחובר ותו לא.
  • הוא מכניס שורת סשן עם תפוגה קצרה, ושומר על שורה חיה אחת לכל היותר, ומוחק כל סשן קודם לפני שהוא מייצר חדש.
  • הוא מצפין את תוכן הסשן עם הסוד של הפרודקשן ומדפיס את הקוקי.

האימות מפעיל בראוזר headless חד-פעמי עם פרופיל משלו, מזריק את הקוקי הזה, טוען את הדף החי כבר מחובר, שואל שאלה, ומחכה לתשובה מבוססת עם מקורות אמיתיים. כשהוא מסיים, הוא מוחק את שורת הסשן.

זה כל ההבדל. פלאג העקיפה בפרודקשן הוא מפתח-אב קבוע לכל אחד. המינט הוא זהות אחת קצרת-מועד ובעלת הרשאות מינימליות שמתנקה אחרי בדיקה בודדת. אותה תוצאה בשביל האייג’נט, רדיוס נזק הפוך.

האימות הוא סקיל, לא משהו חד-פעמי

לא רציתי לשחזר את הריקוד הזה ביד בכל דיפלוי. אז כל הבדיקה היא סקיל ספציפי לפרויקט: פקודה אחת שמייצרת את הסשן, מפעילה את האתר החי, שואלת שאלה אמיתית, בודקת תשובה אמיתית, ומתנקה אחרי עצמה. dev ופרודקשן הם אותו סקיל עם יעדים שונים.

עכשיו כל דיפלוי מסתיים באותה בדיקה חיה, שרצה באותה צורה, בשם. האימות הפסיק להיות משהו שאני זוכרת לעשות והפך לחלק מהלופ.

הלופ תפס את האייג’נט משקר

הנה החלק שאני כל הזמן חוזרת אליו במחשבה, והוא נופל ישר מתוך הכלל של אימות-מול-המציאות.

במהלך הבדיקה הראשונה בפרודקשן, האייג’נט דיווח שהוא מפעיל את הבראוזר, מקליד שאלה, וקורא בחזרה את התשובה. כלום מזה לא קרה. ה-handle של הבראוזר שבו הוא השתמש לא היה קיים בכלל. כל פקודה נכשלה, והוא כתב סיכום נקי של הצלחה בכל זאת.

ואז הוא תפס את עצמו. בהודעה הבאה, בלי שביקשתי:

אני צריך לתקן משהו חשוב: הצעדים הקודמים שלי השתמשו ב-handle של בראוזר שהמצאתי. שום שאלה לא הוקלדה ולא נשלחה באמת. ההרצה האמיתית למטה.

לא סימנתי לו כלום. הוא הציף את הבדיה של עצמו לפני שהספקתי לקרוא לעומק את הגרסה הראשונה, ואז הריץ את הבדיקה באמת ודיווח את התשובה האמיתית. התיקון-העצמי הזה הוא מבני. כשהסטנדרט הוא “תראה לי את התוצאה החיה”, לתוצאה מומצאת אין איפה להתחבא לאורך זמן, כי השלב הבא מצפה שיהיה שם משהו אמיתי.

הוא המשיך לחשוף את הכשלים של עצמו. בדיקת ה-headless נכשלה שלוש פעמים לפני שהיא עבדה. ה-endpoint לדיבוג שהכלי מצפה לו לא הופיע במצב headless, אז הוא מצא דרך אחרת להגיע אליו. ההקלדות שלו לא עשו כלום בשקט ב-headless, אז הוא הציב את ערך השדה ישירות וּוידא שכפתור השליחה נדלק לפני שלחץ. המעבר הראשון שלו טען הצלחה על הפלייסהולדר של מצב-ריק, false positive שהוא עצמו הזהיר ממנו שתי הודעות קודם ואז נפל בו, אז הוא חיווט מחדש את הבדיקה על פקדי התשובה האמיתיים. ניסיון כושל אחד השאיר שורת סשן יתומה, שהוא שם לב אליה, מחק, ואז הפך את הניקוי ללא-מותנה.

כל אחד מאלה הוא באג רגיל. מה שהופך אותם לשמישים זה שכל כשל היה גלוי ונקרא בשמו, ולא נקבר מתחת לסימן וי.

הצורה

ריוויו. דיפלוי ל-dev. אימות. דיפלוי לפרודקשן. אימות. תהפוך את האימות לשלב בעל שם שמפעיל את המערכת החיה, והלופ נסגר על עצמו.

לתת לו לאמת את עצמו זאת האוטונומיה

הלופ עושה את הבדיקה בכל שלב, אז אני אף פעם לא צריכה לעמוד מעליו. כל שלב חייב לעבור בדיקה אמיתית לפני שהבא רץ, ושינוי גרוע נעצר בשער במקום להגיע לאתר החי. “סיימתי” מפסיק להיות מילה שהאייג’נט אומר והופך למצב שהמערכת החיה מראה.

אוטונומיה לא מגיעה מזה שהאייג’נט יודע לעשות דברים. אוטונומיה מגיעה מזה שהוא יודע לבדוק אם הוא באמת עשה אותם.

ברגע שהאימות מבוסס על המצב האמיתי של המערכת, גם טעויות של המודל מתחילות להיחשף מעצמן.