דלגו לתוכן

רב־משתמשים — זהות, חברות ונוכחות

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

ישויות ראשיות: משתמשים וסוכנים הם שחקנים שווי־מעמד

קטע בשם “ישויות ראשיות: משתמשים וסוכנים הם שחקנים שווי־מעמד”

כל דבר שמתעד “מי עשה זאת” — הודעות, כרטיסים, סקירות, תוכניות, יומני הרצה, רשומות ביקורת — נפתר דרך ישות ראשית, איחוד אטום של UserPrincipal ו-AgentPrincipal עם צורה תמציתית ברשת (user:<id> / agent:<id>). ייחוס אינו ממוטט N בני אדם (או N סוכנים) לחשבון מייצג יחיד, ולכן סקירה שהשאיר סוכן וסקירה שהשאיר אדם ניתנות להבחנה בכל מקום שבו הן מופיעות.

מכיוון שהישות הראשית היא יחידת הזהות, היא גם יחידת ההכלה:

  • מגבלות קצב הן לפי ישות ראשית, לא לפי סשן. מגן ההצפה על קריאות כלים מרחוק מתקצב לפי משתמש, לא לפי חיבור: כל המכשירים של חבר יונקים מחלון נע משותף אחד (120 קריאות בדקה, ומהן לכל היותר 30 רשאיות לשנות), כך שפתיחת שלושה מכשירים אינה משלישה לאיש את המכסה וחבר רועש אחד לא יכול להרעיב את האחרים.
  • הביטול הוא לפי ישות ראשית. יומן הפעולות של כל לקוח מתעד רק את השינויים שהישות הראשית שלו ביצעה. אפשר לבטל עם ⌘Z עריכה משלכם בכרטיס; אי אפשר לבטל עריכה של חבר לצוות — עד שתראו אותה, המסלול העמיד כבר הכריע בה (ראו בהמשך).

החברות היא גבול הגישה — לא מפתח הצימוד

קטע בשם “החברות היא גבול הגישה — לא מפתח הצימוד”

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

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

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

תפקיד מה מותר
owner שליטה מלאה, כולל מחיקת סביבת העבודה; בדיוק אחד לכל סביבת עבודה
admin ניהול חברים, הזמנות, הגדרות ופעולות הרסניות
member קריאה וכתיבה יומיומית: הודעות, כרטיסים, הרצות סוכנים
viewer קריאה בלבד בכל סביבת העבודה; תבניות glob להחרגת סודות מסתירות נתיבים תואמים במשטחים הנושאים קוד
guest אותה רצפת קריאה בלבד כמו צופה, בדרגה הנמוכה ביותר בסולם

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

הזמנות הן הדרך להעניק חברות, מתוך הגדרות ← סביבת עבודה ← חברים. מנהל מפיק קוד חד-פעמי (כקישור או כ-QR) שפג תוקפו אחרי שבעה ימים כברירת מחדל; השרת שומר רק את גיבוב ה-SHA-256 של הקוד, והקוד עצמו מוצג פעם אחת בדיוק. מימושו מקצה את המשתמש ב-JIT, רושם חברות בתפקיד של ההזמנה (לעולם לא owner — בעלות מועברת במפורש, ולעולם אינה מוענקת בהזמנה) ומעביר לצימוד המכשיר. פרטי הגישה של מכשיר שמונפקים אחרי הצימוד מוגבלים בעצמם ל-30 יום, כך שצימוד אינו מפתח קבוע.

הענקות לפי מאגר יושבות מתחת לחברות. מכיוון שהשרת מחזיק עותקי עבודה מלאים, החברות בסביבת העבודה לבדה אסור לה שתעניק הרשאה גבוהה מזו של מארח הקוד: כל משטח הנושא קוד — קבצים, diffs, סקירת בקשות משיכה, חיפוש קוד — בודק את ההענקה של החבר על אותו מאגר. ההענקות הן לכל (סביבת עבודה, משתמש, מאגר) ומדורגות none → read → review → write, כאשר write פירושה הפעלת הרצות סוכנים שכותבות למאגר. היעדר שורה פירושו none; בעלים ומנהלים מחזיקים באופן משתמע ב-write על כל מאגר מקושר ואינם נושאים שורות כלל.

שני מסלולים של מצב שיתוף פעולה

קטע בשם “שני מסלולים של מצב שיתוף פעולה”

Control Center מפצל את המצב המשותף לשני מסלולים עם כללי עמידות הפוכים, מפני ששני סוגי המצב נכשלים בדרכים הפוכות.

נוכחות — מי מחובר, היכן הוא נמצא, מה הוא מקליד, מה סוכן עושה כרגע — חסרת ערך ברגע שהיא מיושנת. אילו נשמרה, כל התחברות מחדש הייתה צריכה ליישב בין “מסד הנתונים אומר שסם עורך את הכרטיס הזה” לבין המציאות. לכן נוכחות לעולם אינה נכתבת למסד הנתונים. היא שוכנת במרכז נוכחות בזיכרון בשרת: לקוחות מפרסמים עם presence.update, עמיתים נרשמים עם presence.watch, ורשומות פגות אוטומטית אחרי שלוש פעימות לב שהוחמצו (פעימות הלב רצות כל 10 שניות; המרכז סורק כל 5). אין מה ליישב — רק לתת לפקוע.

שתי תכונות שומרות על מהימנות המסלול:

  • הזהות מגיעה מהסשן, ולעולם לא מארגומנטים של הלקוח. השרת ממפה את המכשיר המאומת של החיבור למזהה משתמש; לקוח יכול תמיד לעדכן רק את הנוכחות של עצמו, כך שאיש אינו יכול להתחזות לסטטוס או למיקום של משתתף אחר. עדכון offline מפורש (ניתוק או מתג ההיעדרות) מסיר את הרשומה מיד.
  • הסגל מרוכז בשרת. עמיתים לעולם אינם מחליפים נוכחות ישירות, כך שכל משתתף רואה את אותו סגל ללא תלות בטופולוגיה. לסוכנים אין לקוח, ולכן השרת מסנתז את הרשומות שלהם ממצב ההרצה החי — חושב, פועל, חסום על שער אישור, סיים — כשהרצה מייצגת אחת מכריעה את המצב והמיקום של הסוכן, כדי שהרצות מקבילות לא יגרמו לסגל להבהב.

הפיזור מדורג לפי צרכן: מחשב ואינטרנט נוסעים ברמה מלאה (לכל היותר פליטת סגל אחת ל-100 מילישניות), והטלפון ברמת תקציר (ל-500 מילישניות), כך שעדכונים בקצב של סמן לא יכולים להתיך את ה-PWA.

המיקום של משתתף — היכן הוא נמצא כעת, עד לרמת החפץ — הוא איחוד אטום: Space, File (מאגר + נתיב + שורה אופציונלית), Pr, Ticket, PlanNode. לא לכל מיקום יש לאן לקפוץ: אין נתיב של מציג קבצים, ולכן מיקום File מוצג כתווית ולא כיעד. המסלול נושא גם הצהרות בעלות רכות (“סם עורך את הכרטיס הזה”, “סוכן מחזיק את עץ העבודה הזה”): נראות של התנגשות, לא נעילות. סוגי מיקום לא מוכרים מנותחים ל-null, כך שלקוח ישן פשוט אינו מציג את מה שאינו מבין.

רשומות שחייבות לשרוד — כרטיסים, מסמך ההערות המשותף של המרחב, הודעות — הולכות במסלול ההפוך: סמכותיות, מסודרות ונשמרות. עריכות מקבילות מוכרעות לפי “הכותב האחרון מנצח” לפי סדר קבלה בשרת, ולעולם לא לפי שעונים של לקוחות. ההכרעה מכוונת לעדינות: עריכות בכרטיס הן לפי עמודה, כך ששני אנשים שעורכים שדות שונים של אותו כרטיס מגיעים ליעדם ואף אחד לא מוחק את של האחר (מעברי זרימת עבודה שומרים במקום זאת על נעילה אופטימית); מסמך ההערות של המרחב הוא עמודה אחת, ולכן כל המסמך כפוף ל-LWW, כשהצהרות בעלות רכות במסלול הנוכחות הופכות עריכה מקבילה לגלויה במקום לנעולה. הוא במכוון אינו CRDT — נטל ההוכחה מוטל על ה-CRDT, ו-LWW לפי סדר קבלה בתוספת הצהרות גלויות מכסה את דפוסי שיתוף הפעולה בפועל.

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

מעקב, היגוי, השתלטות והחזרת שליטה

קטע בשם “מעקב, היגוי, השתלטות והחזרת שליטה”

מסלול הנוכחות אינו רק סגל — הוא מניע את פעלי שיתוף הפעולה:

  • מצב מעקב. לוחצים על משתתף והלקוח שלכם רוכב על המיקום שלו ומנווט כפי שהוא מנווט. המעקב הוא “עד שתפעלו”: ברגע שאתם מנווטים בכוחות עצמכם, המעקב מתנתק. באנר שניתן לסגור מודיע כשמשתתף אחר עושה זרקור (מציג) על מרחב שאתם נמצאים בו.
  • היגוי. הודעה לסוכן פעיל נכנסת לתור כהיגוי; הסוכן קורא אותה מחדש לפני שהוא ממשיך לתור הבא שלו, כך שאפשר לדחוף הרצה באמצע הדרך בלי להרוג אותה.
  • השתלטות. נטילת שליטה בשיחה מפסיקה כל הרצה פעילה בגבול התור הנקי הבא שלה (הרצת Claude Code, שאין לה גבול בטוח, נעצרת) מאחורי תמונת מצב של git לכל תור, כך שההשתלטות הפיכה. סמן עמיד חוסם שליחות חדשות למרחב ושורד הפעלה מחדש של השרת — השרת לעולם אינו ממשיך אוטומטית לתוך עריכה חצי-גמורה של אדם. עורך חי אחד לכל עץ עבודה הוא הצהרת בעלות רכה: השתלטות שנייה נראית ומסורבת, ולא ממוזגת בשקט.
  • החזרת שליטה. החזרת השליטה מפרסמת סיכום diff מובנה של עץ העבודה למרחב, מכניסה אותו לתור כהיגוי עבור ההרצות המושהות, ממשיכה אותן ומנקה את הסמן.

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

רמה ערך ברשת השפעה על קריאת כלי מגודרת
הצעה בלבד proposeOnly נדחית על הסף, עם סיבה שאומרים לסוכן להעביר הלאה — הוא יכול לתאר את הפעולה אך לא לבצע אותה
פעולה באישור actWithApproval שער האישור שכשלו סגור: בקשת אישור אחת מוצגת למפעיל מחובר, ואין מאשר פירושו דחייה
פעולה חופשית actFreely מאושרות מראש, למעט מקום שבו חל כלל deny קשיח ממעקות הבטיחות — כלל דחייה תמיד מנצח
(לא מוגדר) null זהה לפעולה באישור: ברירת המחדל של השרת

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

אפס רגרסיה למפעיל בודד

קטע בשם “אפס רגרסיה למפעיל בודד”

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