שפהעברית

ישיבה מרחוק

שמור את URL וטוקן מגוון ממצגת אחרת של AirCode Ø אישי, ולאחר מכן בחר את החיבור הזה ב- ישיבה חדשה → פעל ב-. הדוגמה הרחוקה מבקשת את Claude Code, Codex או ספק מקורי אחר זמין. הלקוח הנוכחי שלך מציג את השיחה והטרמינל כפי שהסוכן הרחוק עובד.

בחרו היכן מתגורר הפרויקט. עם מיקום של פרוייקט → דוגמה זו, A מחזיקה בפרויקט P1 ו- B פועלת על הסוכן באמצעות חשבון ספק של B. B מבקש את הקבצים הדרושים באמצעות כלים של חלל עבודה; A קורא, מעריך ומפעיל פקודות במדריך P1 בפועל. אין שום קלון או סינכרון סופי. הקוד וההוצאה של הפקודה הנדרשים מגיעים ל-B ולספק AI שלה. חשבונאות ושימוש של ספקים להישאר על B. הבדיקות, הבדיקות והבדיקות נמשכות על א.

A פותח חיבור WSS מאושר ל- B, אשר נושאת שיחות כלי בשני הכיוונים. המכונות יכולות להיות ברשתות שונות או במרכז נתונים. A אינו זקוק לפורט נכנס או לכתובת ציבורית; B זקוק לאינסטינקט HTTPS זמין URL, והפרוסיו חייב לתמוך בהשדרוגים של WebSocket. שני המקרים חייבים לתמוך באזורים עבודה מחוברים. סגירת הדפדפן או התצוגה הניידת משאירה את חיבור השרת הזה במקום. חייב להישאר פועל עבור הסוכן להשתמש בפרויקט שלו.

מיקום הפרויקט → דוגמה מרחוק שומרת על המצב השני: גם הסוכן וגם הפרויקט הרשמי של הטוקן נמצאים ב- B. שתי המגמות הללו הן מפורשות; בחירת פרויקט בשם דומה ב-B לעולם לא פירושה P1 ב-A.

חיבור שני האינסטציות שלך

  1. בדוגמה הרחוקה, חברו את חשבון הספק המקורי שלכם ב- AI שימוש.
  2. פתחו הגדרות → שער AI, ובנייד עוד → שער AI. תחת יצירת גישה הפעילו אפשר חיבורים פרטיים. תחת יצירת אסימון השאירו את ברירת המחדל מופע אחר, תנו שם לחיבור ובחרו יצירת אסימון חיבור. כברירת מחדל הקבצים והפקודות נשארים ב־A, והסוכן משתמש בחשבון הבינה המלאכותית ב־B. כדי לעבוד על הקבצים ב־B, פתחו היכן יעובדו קבצים ויורצו פקודות?, בחרו המופע הזה ופרויקט ב־B. בחירה לכל הפעלה מאפשרת את שני המקומות באותה הגבלת פרויקט ב־B. אסימון AI API מיועד ליישומים בלבד.
  3. העתק את הטוקן מההצהרה הפעם שלה ואת Base URLהמוצג.
  4. ב־A פתחו הגדרות → שער AI → הוספת שרת אחר. הזינו שם חיבור, את URL של המופע B או Base URL שלו ואת האסימון, ובחרו שמירת חיבור. אפשר גם להוסיף דרך הפעלה חדשה → הפעלה על → הוספת מופע מרוחק….
  5. ב- ישיבה חדשה, בחר את החיבור המאוחסן תחת Run on. בדוק את מיקום הפרויקט כברירת מחדל ל instance כאשר הסימן מאפשר זאת. שמור או בחר P1 תחת Project על הדוגמה הזאת, בחר Harness → Model → מאמץ → הרשאות, הזן את ההזמנה, ולאחר מכן בחר התחל את הפגישה. מתאם העבודה הנוכחי תומך ב- Claude Code ו- Codex; זמין הבחירות מגיעות מהקטלוג החי של B. מנועים אחרים שומרים על מצב פרויקט מרחוק.

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

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

Codex דורש כרגע כניסה מקורית עם תמיכה בקובץ (auth.json) ב- B עבור מצב זה; כניסה עם מפתח בלבד נדחתה באופן מפורש. הגדרת הפרטיות שלה משתמשת בקישורים לקובץ החשבון המקורי ולקובצי השיחה, מבלי להעתיק ערכים של OAuth. CLI תאימות ומדיניות המנוהלות על ידי מכונה עדיין חל.

עבודה עם סוכן מרחוק

מדריך ליישום ולשאול כל דבר

פתחו את הדלת או את הדלת על A, פתחו את מודל (הצג הגדרות הסוכן ב-composer), ולאחר מכן בחרו את החיבור B שנשמר תחת פעל על. בחר Harness → Model → Effort → Permissions מתוך קטלוג של B. באפשרותך גם לשמור חיבור שם עם הוסף דוגמה מרחוק…. שתי הדוגמאות דורשות פרסום גרסה של conversationWorkspace: 1; פתיחת בלוקים B חסרים או לא זמינים.

עבור פקודה ליישום, תיאר את היישום ולאשר את שמות היישום / הפרויקט שלו כרגיל. הפרויקט, האפליקציה שנשמרה, המפקח לפיתוח ותצוגה מוקדמת שייכים ל-A. הודעות מעקב, עריכה חזותית ואזורים נבחרים של צילומי מסך הולכים לסוכן B. Plan first → Single agent can submit its Plan checkpoint on A; Convergence currently requires This instance.

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

דלתות אלה שומרות על זהויות שיחה וסיפורים רגילים. פתיחת שיחה נגמרת ובחירת Resume מחזירה באופן מפורש את אותו הקשר המקורי ב- B; חיבור מחדש לרשת רק מחזיר מחדש את הקוראים. קבצים מצורפים, פקודה פתיחה ואת הפרטי תת-מגמה העתקה נשארים זמינים על A. הגדרות הסוכן יכולות לשנות את הרשאות חלל העבודה של A ואת המודל / המאמץ של B. הטירמינל מציג את B והוא קריא בלבד; פקודות פועלות על A והן מופיעות בשיחה. דפדפן, תכנון וכלים Knowledge מוצגים על פי דרישה באמצעות יכולות A קיימות של השיחה. אין עוזר או גשר מוסיפים לשיחה מקומית רגילה על ידי תכונה זו.

צ’אט מציג אירועים טרנסקיפט חדשים ודיאלוגים הרשאה / שאלה הנוכחיים. לשלוח הודעות מעקב, לענות על הדיאלוג, או להשתמש Stop כדי להפסיק את התשובה הנוכחית. במצב עבודה מחובר, הרשאות מיוצגות על ידי A: מצבי קריאה בלבד מסרבים לכתוב ולבצע פקודות; מצבי בקשה רגילים בשיחה זו; מצבי אישור עריכה מאפשרים עריכת קבצים אך מבקשים פקודות קשת; מעבר מאפשר את הפעולות המבוקשות. טופס עבודה מרחוק של הפגישה החדשה נפרד מתקן מצב הרשאה בעת הפעלת הפעלת הפעלת הפעלת הפעלת הפעלת הפעלת הפעלת הפעלת הפעלת הפעלת הפעלת הפעלת הפעלת הפעלת הפעלת הפעלת הפעלת הפעלת הפעלת הפעלת הפעלת הפעלת הפעלת הפעלת הפעלת הפעלת הפעלת הפעלת הפעלת הפעלת הפעלת הפעלת הפעלת הפעלת הפעלת הפעלת הפעלת הפעלת הפעלת הפעלת הפעלת הפעלת הפעלת הפעלת הפעלת הפעלת הפעלת הפעלת הפעלת. סוכן טרמינל (קריאה בלבד) מציג את היציאה בפועל של ספק B; פקודות פרויקטים פועלות על A דרך כלים חלל עבודה ולהעביר את היציאה שלהם בצ’אט. במצב פרוייקט מרחוק, Terminal גם מקבל כניסה מקורית ושינוי גודל.

הטרמינל מספק ייצור חי דרך WebSocket. צ’אט משתמש בתגובה SSE מתמשכת ומפרסם אירועים בזמן שהספק כותב את התרגום שלו; חלק מהספקים רשומים הודעה מלאה ולא כל טוקן. זה לא מבטיח עדכוני צ’אט ברמה של טוקן עבור כל CLI. טקסט נפרד-רק /chat/completions API עדיין משתמש מברשת SSE.

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

סגירת התצוגה מפרקת רק את הקורא שלה. הסרת החיבור שנשמר סוגרת את גישת חלל העבודה שלו ומפסיק את הפקודות הבעלים שלו ב- A; זה שומר על שיחה של B. סיום הפגישה מפסיק את הסוכן ואת הפקודות הבעלים שלו; התרגום שלו נשאר באירוע מרחוק וניתן לפתוח אותו מחדש לקריאה. התחל פגישה חדשה כדי להפעיל שוב סוכן לאחר סגירתו. שיחות מרחוק שנשמרו מופיעות תחת החיבור שלהם בסדרה של הפגישה החדשה. סעיף זה מתאר את הטופס המרוחק New Session נפרד. דחיפה לאפליקציה ושאל כל דבר לשמור על ההיסטוריה הרגילה שלהם ופעולה חידוש מפורשת על A.

טווח, ביטול ומגבלות

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

גישה למרחבי עבודה מחוברים ניתנת כברירת מחדל לאסימונים חדשים של מופע אחר; היא לעולם אינה מתווספת אוטומטית לאסימונים ישנים. כל יציאה על A מחברת זהות של תיקיה של פרויקט אחד, חיבור, מצב הרשאה וכוונה. B אין API לשנות את השורש הזה. מסלולי הקובץ הם יחסיים, קישורים חיצוניים מסרבים, וכתובים דורשים את SHA-256 שנצפה בעת קריאת הקובץ. דוחות קבועים או לא ברורים נדחו. פקודות shell מאושרות יש את הרשאות המשתמש של A; פרויקט cwd הוא לא sandbox של מערכת הפעלה.

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

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

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

שטחי עבודה מחוברים מוסיפים עד 20 ערוצים של כלים קבועים ב-B וארבעה לכל טוקן, עם ארבעה שיחות בטיסה וארבעה פקודות פרוייקט פועלות לכל שטח עבודה. A מאפשר 20 מרחבי עבודה פעילים ו-20 פקודות בסך הכל. הקווים מוגבלים ל-2 MB, קבצים רגילים / תוצאות של עריכה ל-8 MB, שיחות ל-20,000 לכל שטח עבודה, ופקודות ל-10 דקות. הכלי מגיב לתוכן קובץ הדף ומציין באופן מפורש זרועות פקודה משולבות. דפי קריאה הם עד 128 KiB; ארגומנטים טקסט כתיבה / תיקון יש גבולות התכנית שלהם חייבים להתאים לתוך מסגרת אחת. אלה הם גבולות משאבים מקומיים; אין מקור ציבורי נוסף נוצר.

Session API עבור גיבוי אישי או כלי AI

הסכמה של הפגישה OpenAPI מתארת פרוטוקול זה. השתמש ב- Authorization: Bearer TOKEN בכל שיחה אל /api/ai-gateway/v1 Base URL של האירוע המפיץ. מסרב להעביר; לעולם אל תשתמש ב- URL/query credentials. אל תכניסו את ה-Token לתוך הזמנות, קוד מקור או אחסון דפדפן.

נקודת סיום פירוש
GET /session-catalog גרסה של פרוטוקול , פרוייקט מורשה אופציונלי, workspaceAccessוקטלוג המנוע / המודל / המאמץ / הרשות
POST /sessions התחל סוכן מקומי; נדרש UUID intention, engine, model, permissionMode, initialPrompt; אופציונלי effort, name
GET /sessions עד 100 שיחות אחרונות שנוצרו על ידי טוקן זה
GET /sessions/{id} סיכום נוכחי של שיחה בבעלות טוקן זה
POST /sessions/{id}/resume המשך מפורש של שטח עבודה של שיחה עם UUID intention, initialPrompt, אופציונלי model/effort; אותו הקשר B מקורי
POST /sessions/{id}/events Continuous SSE; JSON { "offset": null } בהתחלה, ולאחר מכן החזר האחרון מחליף על חיבור מחדש
POST /sessions/{id}/transcript לקרוא דף עם שם; לשלוח offset עבור אירועים חדשים או before עבור הודעות קודמות
POST /sessions/{id}/input Follow-up: UUID intention ו- non-empty text
POST /sessions/{id}/interaction UUID intention, דיאלוג הנוכחי id, ואת האפשרות המומלצת / טקסט / תשובה
POST /sessions/{id}/command UUID intention ו- action: interrupt, close, model, effort או permission; שינויים בבחירה דורשים arg מהקטלוג
GET /sessions/{id}/terminal אושר WebSocket עדכון; יציאת מסוף גלם, JSON input { "t": "i", "d": "text" }, { "t": "r", "c": 100, "r": 30 } resize
GET /workspaces/{id} לשחזר את הפגישה של B על ידי מרחב העבודה המקורי UUID; לעולם לא ליצור תחליף על תגובה אבודה
GET /workspaces/{id}/channel A פותח WebSocket מאושר ל-B לפני ההפעלה; B שולח שיחות כלי חלל עבודה, A מחזיר את התוצאות שלהם

עבור גשר A-workspace המובנה, השתמש ב- API המאושר על ידי הבעלים של A מהלקוח הנוכחי: שמור את B עם POST /api/ai-gateway/connections, לגלות אפשרויות עם GET /api/engines?remoteConnectionId=ID&workspaceLocation=local, ולאחר מכן להתקשר ל- POST /api/ai-gateway/connections/ID/sessions עם localProject: "P1" ואת הבחירה של קטלוג. A מספק את זהות חלל העבודה ואת ערוץ הפוך. השם המוחזר agremote_… פותח את /api/ai-gateway/remote-sessions/NAME/events של A ו- /interaction, כולל אישור על A. טוקיו של שער B אינו יכול לאשר את נקודות הקצה של הבעלים האלה. הדוגמה הבאה מראה את ההבדל המלא הזה.

עבור פקודה ליישום ו- לשאול כל דבר, השתמש ב- POST /api/sessions המאושר על ידי הבעלים הרגיל ב- A עם remoteConnectionId: "SAVED_CONNECTION_ID" ובחירת קטלוג של B. השתמש askAnything: true עבור שאלה, או project ו- createMode: true עבור פרויקט יישום קיים. כדי ליצור יישום חדש, השתמש POST /api/create-mode/projects/confirmed עם prompt, appName ו- projectName. שמור על idempotencyKey במהלך הפעלה לא בטוחה. התשובה כוללת את מטא-נתונים הרגילים של A: tmuxName, sessionId ו- remoteAgent . מעקב, טרנסקפט, שטח עבודה וסיקור משתמשים ב- A’s normal conversation APIs; לעולם לא להחליף את ה- gateway token עבור A’s owner credential.

אם אתה מיישם שרת חיבור נפרד, עיין בפרוטוקול שטח עבודה ותכניות כלי. הפעל ב-B עם workspace: { id: UUID, project: "P1" } רק לאחר יישום הערוץ, שורש מקומי מקושר, הרשאות וקבלות הפעלה קבועות. תווית אחת בלבד אינה מספקת גישה לקובץ. ההכנסה של B והשינויים ברשיונות הליבה אינם זמינים עבור הפגישות האלה.

SSE update מכיל { id, project, session, transcript }. transcript.events היא דף האירועים החדש, offset הוא הקורטור של מספרים שלמים בלתי נתפסים, ו- interaction או terminalAttention מתאר את הצרכים הנוכחיים של הכניסה. שמור על מסגרות SSE מלאות על פני חתיכות רשת. תתעלמו מההערות של שומרים. באירוע error , שמור את השיחה והתחבר מחדש לקריאה. לעולם אל תסמכו כי פעולה לא התרחשה מתגובה רשת אבודה.

מצבי הרשאה, מזהות מודל ומאמצים מגיעים מ- /session-catalog, ולא מזהות מודל מורכב של טקסט API. יצירת הפגישה ופעולות דחו שדות שאינם נתמכים. ההזמנות הראשונות וההמשך מוגבלות ל- 64 KiB. כוונות ההשקה מתרחבות ל- token; כוונות הפעולה מתרחשות ל- session. שימוש חוזר של כוונה עם תוכן שונה נדחה. לאחר ניסיון משלוח זמן הפעלה, תוצאה לא ידועה נשארת AI_GATEWAY_SESSION_UNCERTAIN ולא ניתן לשחק מחדש באופן אוטומטי.

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

פנו אל האפליקציה ושאלו כל דבר על A

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

// Node.js 22+. Private backend only; never send these credentials to the website.
const origin = new URL(process.env.AIRCODE_LOCAL_ORIGIN);
const ownerToken = process.env.AIRCODE_LOCAL_AUTH_TOKEN;
const connectionId = process.env.AIRCODE_REMOTE_CONNECTION_ID;
const idempotencyKey = process.env.AIRCODE_REMOTE_INTENTION;
if (!['https:', 'http:'].includes(origin.protocol) || origin.username || origin.password
    || origin.pathname !== '/' || origin.search || origin.hash
    || !ownerToken || !connectionId || !idempotencyKey) throw new Error('Configure A, its saved B connection and one retained intention UUID');
async function call(path, body) {
  const response = await fetch(origin.origin + '/api' + path, {
    method: body === undefined ? 'GET' : 'POST', redirect: 'error',
    headers: { authorization: 'Bearer ' + ownerToken, 'content-type': 'application/json' },
    ...(body === undefined ? {} : { body: JSON.stringify(body) }),
  });
  if (!response.ok) throw new Error('A refused the operation: HTTP ' + response.status);
  return response.json();
}
const catalog = await call('/engines?remoteConnectionId=' + encodeURIComponent(connectionId) + '&workspaceLocation=local');
const engine = process.env.AIRCODE_REMOTE_ENGINE, model = process.env.AIRCODE_REMOTE_MODEL;
const permissionMode = process.env.AIRCODE_REMOTE_PERMISSION;
if (!engine || model === undefined || !permissionMode) {
  console.log(JSON.stringify(catalog, null, 2));
  throw new Error('Choose harness, model and permissions from B’s catalogue');
}
const selection = { remoteConnectionId: connectionId, engine, model, permissionMode,
  ...(process.env.AIRCODE_REMOTE_EFFORT ? { effort: process.env.AIRCODE_REMOTE_EFFORT } : {}) };
// Set AIRCODE_APP_PROJECT only for an existing A application project.
// To create one first, POST /create-mode/projects/confirmed with confirmed
// { projectName, appName, prompt }, then retain its returned name.
const project = process.env.AIRCODE_APP_PROJECT;
const session = await call('/sessions', { ...selection, idempotencyKey,
  ...(project ? { project, createMode: true } : { askAnything: true }),
  initialPrompt: process.env.AIRCODE_PROMPT || 'Explain how this distributed conversation works.',
});
console.log('Conversation on A:', session.tmuxName, 'Agent:', session.remoteAgent.instanceName);
// Read-only observation; never replay the launch or prompt to reconnect.
const transcript = await call('/sessions/' + encodeURIComponent(session.tmuxName) + '/transcript');
for (const event of transcript.events) if (event.text) console.log(event.text);
// Poll the same route with ?offset=LAST_OFFSET for incremental updates.
// Send an explicit follow-up to POST /sessions/NAME/input with { text }.
// Resume explicitly with POST /sessions { project: session.project,
//   resumeSessionId: session.sessionId, initialPrompt: 'Continue' }.

פרוייקט על A, סוכן על B

השתמש בפרוטוקול של A's owner in a private backend. ה-Gateway Token של B כבר נשמר על A ולא יכול להחליף את אישור הבעלים הזה. בחר את ערכי הקטלוג ולשמור UUID כוונה אחת.

import { randomUUID } from 'node:crypto';

// A owns P1. B supplies the agent/account. Never send these values to the website.
// Save B once in A's Settings → AI gateway → Remote instances.
const origin = process.env.AIRCODE_LOCAL_ORIGIN;
const ownerToken = process.env.AIRCODE_LOCAL_AUTH_TOKEN;
const connectionId = process.env.AIRCODE_REMOTE_CONNECTION_ID;
if (!origin || !ownerToken || !connectionId) throw new Error('Configure A and its saved connection to B');
const url = new URL(origin);
if (!['https:', 'http:'].includes(url.protocol) || url.username || url.password
    || url.search || url.hash || url.pathname !== '/') throw new Error('Use A’s instance origin');
async function call(path, body, signal) {
  const response = await fetch(url.origin + path, {
    method: body === undefined ? 'GET' : 'POST', redirect: 'error', signal,
    headers: { authorization: 'Bearer ' + ownerToken, 'content-type': 'application/json' },
    ...(body === undefined ? {} : { body: JSON.stringify(body) }),
  });
  if (!response.ok) throw new Error('A refused the operation: HTTP ' + response.status);
  return response;
}
const id = encodeURIComponent(connectionId);
const catalog = await (await call('/api/engines?remoteConnectionId=' + id + '&workspaceLocation=local')).json();
const engine = process.env.AIRCODE_REMOTE_ENGINE;
const model = process.env.AIRCODE_REMOTE_MODEL;
const permissionMode = process.env.AIRCODE_REMOTE_PERMISSION;
if (!engine || model === undefined || !permissionMode) {
  console.log(JSON.stringify(catalog, null, 2));
  throw new Error('Choose harness, model and permissions from B’s catalogue');
}
// Persist the intention before launch and reuse it if its response is lost.
const intention = process.env.AIRCODE_REMOTE_INTENTION;
if (!intention) throw new Error('Set AIRCODE_REMOTE_INTENTION to a new UUID once, then retain it');
const session = await (await call('/api/ai-gateway/connections/' + id + '/sessions', {
  intention, localProject: process.env.AIRCODE_LOCAL_PROJECT || 'P1',
  engine, model, permissionMode, effort: process.env.AIRCODE_REMOTE_EFFORT || '',
  initialPrompt: 'Read the project instructions, then explain which machine owns its files.',
})).json();
console.log('Conversation on A:', session.tmuxName);
const path = '/api/ai-gateway/remote-sessions/' + encodeURIComponent(session.tmuxName);
// Approvals from A appear as transcript.interaction with id "workspace:…".
// Only after the owner chooses an answer, send it to A:
async function answerCurrentInteraction(interactionId, option) {
  return call(path + '/interaction', { intention: randomUUID(), id: interactionId, option });
}
// This bounded reader displays complete events and pending owner decisions.
// It never approves an operation automatically.
const signal = AbortSignal.timeout(30_000);
const stream = await call(path + '/events', { offset: null }, signal);
const decoder = new TextDecoder(); let buffer = '';
try {
  for await (const chunk of stream.body) {
    buffer += decoder.decode(chunk, { stream: true }).replace(/\r\n/g, '\n');
    let boundary;
    while ((boundary = buffer.indexOf('\n\n')) >= 0) {
      const frame = buffer.slice(0, boundary); buffer = buffer.slice(boundary + 2);
      const data = frame.split('\n').filter(line => line.startsWith('data:'))
        .map(line => line.slice(5).trimStart()).join('\n');
      if (!data) continue;
      const update = JSON.parse(data);
      console.log(JSON.stringify(update));
      // Display update.transcript.interaction and let the owner choose an answer.
    }
  }
} catch (error) { if (!signal.aborted) throw error; }
// Disconnecting the reader leaves the agent and workspace connection running.
// An explicit POST path + '/command' with {intention:UUID,action:'close'} stops them.

פגישה חיה על פרויקט ב'

Node.js 22 או מאוחר יותר הגדר את AIRCODE_REMOTE_BASE_URL ו- AIRCODE_REMOTE_TOKEN בתצורה של שרת פרטי. ההפעלה הראשונה מדפיקה את הקטלוג עד שתבחר גם AIRCODE_REMOTE_ENGINE, AIRCODE_REMOTE_MODEL ו- AIRCODE_REMOTE_PERMISSION. AIRCODE_REMOTE_EFFORT הוא אופציונלי. הקורא נסגר לאחר 30 שניות; הסוכן הרחוק ממשיך לפעול.

import { randomUUID } from 'node:crypto';

// Node.js 22+. Configure these privately on your application's server.
const base = process.env.AIRCODE_REMOTE_BASE_URL;
const token = process.env.AIRCODE_REMOTE_TOKEN;
if (!base || !token) throw new Error('Configure the remote Base URL and token');
const url = new URL(base);
if (!['https:', 'http:'].includes(url.protocol) || url.username || url.password
    || url.search || url.hash || url.pathname !== '/api/ai-gateway/v1') {
  throw new Error('Use the instance Base URL without a trailing slash');
}
async function call(path, body, signal) {
  const response = await fetch(base + path, {
    method: body === undefined ? 'GET' : 'POST', redirect: 'error', signal,
    headers: { authorization: 'Bearer ' + token, 'content-type': 'application/json' },
    ...(body === undefined ? {} : { body: JSON.stringify(body) }),
  });
  if (!response.ok) throw new Error('Remote request refused: HTTP ' + response.status);
  return response;
}
const catalog = await (await call('/session-catalog')).json();
const engine = process.env.AIRCODE_REMOTE_ENGINE;
const model = process.env.AIRCODE_REMOTE_MODEL;
const permissionMode = process.env.AIRCODE_REMOTE_PERMISSION;
if (!engine || model === undefined || !permissionMode) {
  console.log(JSON.stringify(catalog, null, 2));
  throw new Error('Choose harness, model and permissions from this catalogue');
}
// Keep this intention for an explicit retry of this exact launch request.
const intention = process.env.AIRCODE_REMOTE_INTENTION || randomUUID();
const session = await (await call('/sessions', {
  intention, engine, model, permissionMode,
  effort: process.env.AIRCODE_REMOTE_EFFORT || '',
  initialPrompt: 'Describe the project without changing its files.',
})).json();
console.log('Remote conversation:', session.id);
// Demonstrate a bounded live reader. Disconnecting leaves the agent running.
const signal = AbortSignal.timeout(30_000);
const stream = await call('/sessions/' + session.id + '/events', { offset: null }, signal);
const decoder = new TextDecoder();
try {
  for await (const chunk of stream.body) process.stdout.write(decoder.decode(chunk, { stream: true }));
} catch (error) { if (!signal.aborted) throw error; }