Sessions distantes
Enregistrez l’URL et un token à portée limitée d’une autre instance personnelle AirCode Ø, puis choisissez cette connexion dans Nouvelle session → Exécuter sur. L’instance distante lance Claude Code, Codex ou un autre agent disponible avec son propre compte fournisseur. Votre client affiche la conversation et le terminal pendant que l’agent travaille.
Choisissez où se trouve le projet. Avec Emplacement du projet → Cette instance, l’instance A possède le projet P1 et l’instance B exécute l’agent avec son compte fournisseur. B demande les fichiers nécessaires au moyen des outils de l’espace de travail ; A lit et modifie les fichiers et exécute les commandes dans le véritable répertoire de P1. Il n’y a ni clonage ni synchronisation finale. Le code demandé et la sortie des commandes parviennent à B et à son fournisseur d’IA. Les identifiants du fournisseur et le suivi de l’utilisation restent sur B. Les dépendances, compilations et tests restent sur A.
A ouvre une connexion WSS authentifiée vers B pour transporter les appels d’outils dans les deux sens. Les machines peuvent être sur des réseaux différents. A n’a besoin ni de port entrant ni d’adresse publique ; B doit avoir une URL HTTPS accessible et un proxy qui accepte WebSocket. Les deux instances doivent prendre en charge les espaces de travail connectés. Fermer le navigateur ou l’application mobile ne coupe pas cette connexion entre serveurs. A doit rester en marche pour que l’agent accède au projet.
Avec Emplacement du projet → Instance distante, l’agent et le projet autorisé par le token sont tous deux sur B. Ces modes sont distincts : un projet de même nom sur B n’est jamais le projet P1 de A.
Connecter deux instances
- Sur l’instance distante, connectez un compte fournisseur compatible dans Consommation IA.
- Ouvrez Paramètres → Passerelle IA, ou Plus → Passerelle IA sur mobile. Sous Créer un accès, activez Autoriser les connexions privées. Dans Créer un token, laissez Une autre instance sélectionné, nommez la connexion et choisissez Créer un token de connexion. Par défaut, les fichiers et commandes restent sur A et l’agent utilise le compte IA de B. Pour travailler sur les fichiers de B, ouvrez Où travailler sur les fichiers et exécuter les commandes ?, choisissez Cette instance et sélectionnez un projet sur B. Choisir par session permet les deux emplacements avec ce même projet sur B.
- Copiez le token affiché une seule fois et l’URL de base indiquée.
- Sur l’instance A, ouvrez Paramètres → Passerelle IA → Ajouter un serveur. Saisissez un nom, l’URL de B ou son URL de base, ainsi que le token, puis choisissez Enregistrer la connexion. Vous pouvez aussi passer par Nouvelle session → Exécuter sur → Ajouter une instance distante….
- Dans Nouvelle session, choisissez cette connexion sous Exécuter sur. Vérifiez que le champ Emplacement du projet indique Cette instance par défaut lorsque le token le permet. Choisissez P1 sous Projet sur cette instance, puis Harnais → Modèle → Effort → Permissions. Saisissez votre demande et choisissez Démarrer la session. Les espaces de travail connectés prennent actuellement en charge Claude Code et Codex ; leurs choix proviennent du catalogue en direct de B. Les autres agents restent disponibles en mode projet distant.
Le token est envoyé une fois au serveur A, qui le conserve sous forme chiffrée. Les clients web et natifs utilisent ensuite un identifiant de connexion. Ce site public ne reçoit ni l’URL ni le token. Pour une connexion par Internet, utilisez HTTPS ; sur un réseau privé, utilisez uniquement une connexion de confiance. Les tokens de passerelle sont des identifiants personnels, pas des liens à partager.
Le pont entre espaces de travail et ses outils MCP ne démarrent que pour un agent distant avec Emplacement du projet → Cette instance. Une session ordinaire ne démarre aucun de ces services. Le pont privé s’arrête lorsque la dernière session distribuée se termine ; la fermeture de sa vue web ou mobile laisse la session active.
Pour ce mode, Codex exige actuellement sur B une connexion native stockée dans un fichier (auth.json). Une connexion limitée au trousseau de clés est refusée explicitement. La configuration privée crée des liens vers les fichiers natifs de compte et de conversation, sans copier les valeurs OAuth. Les règles de compatibilité des CLI et de gestion de la machine restent applicables.
Travailler avec l’agent distant
Création d’application et questions libres
Sur A, ouvrez Création d’application ou Posez votre question, puis Modèle, le réglage de l’agent dans la zone de saisie. Choisissez la connexion B sous Exécuter sur, puis Harnais → Modèle → Effort → Permissions dans le catalogue de B. Vous pouvez aussi enregistrer une connexion avec Ajouter une instance distante…. Les deux instances doivent annoncer conversationWorkspace: 1 ; un token sans la portée nécessaire ou une instance B indisponible empêche le lancement.
Dans Création d’application, décrivez l’application et confirmez son nom et celui du projet. Le projet, l’application enregistrée, le superviseur de développement et l’aperçu restent sur A. Les messages suivants, les modifications visuelles et les zones sélectionnées d’une capture d’écran sont transmis à l’agent sur B. Plan d’abord → Agent unique peut soumettre sa proposition de plan sur A ; Convergence exige actuellement Cette instance.
Dans Posez votre question, saisissez votre question et joignez vos fichiers. Le dossier privé de la conversation et son inventaire Fichiers restent sur A. L’agent peut lire les images et PDF joints par les outils de l’espace de travail. Convertir en projet copie ce dossier vers un projet ordinaire sur A.
Ces deux entrées conservent l’identité et l’historique habituels des conversations sur A. Pour reprendre une conversation terminée dans le même contexte natif sur B, ouvrez-la et choisissez explicitement Reprendre. Une reconnexion réseau ne fait que reconnecter les lecteurs. Pièces jointes, sorties de commandes et transcription privée progressive restent accessibles sur A. Les réglages de l’agent peuvent modifier les autorisations de l’espace de travail sur A et le modèle ou l’effort sur B. Le terminal montre B en lecture seule ; les commandes s’exécutent sur A et apparaissent dans Chat. Les outils du navigateur, de planification et de Knowledge sont proposés selon les capacités déjà disponibles dans la conversation sur A. Ce mode n’ajoute ni assistant ni pont aux conversations locales ordinaires.
Chat affiche les nouveaux événements et les demandes actuelles d’autorisation ou de réponse. Vous pouvez envoyer un message, répondre à une demande ou utiliser Arrêter pour interrompre la réponse en cours. En mode espace de travail connecté, A applique les autorisations : un mode lecture seule refuse les écritures et les commandes ; un mode normal demande l’accord dans la conversation ; un mode d’approbation des modifications autorise l’édition de fichiers mais demande l’accord pour les commandes shell ; le mode Contourner autorise les opérations demandées. Le formulaire distinct de session distante fixe le mode d’autorisation au lancement. Les deux entrées de conversation précédentes exposent le contrôle habituel des autorisations de A.
Terminal de l’agent (lecture seule) montre la sortie réelle de l’agent sur B. Les commandes sur le projet s’exécutent sur A par les outils de l’espace de travail et leur sortie apparaît dans Chat. En mode projet distant, Terminal accepte aussi la saisie et le redimensionnement.
Le terminal transmet la sortie en direct par WebSocket. Chat utilise une réponse SSE continue et publie les événements à mesure que le fournisseur écrit sa transcription ; certains fournisseurs n’enregistrent un message qu’une fois terminé. Chaque CLI ne fournit donc pas forcément une mise à jour par token. L’API texte distincte /chat/completions conserve son flux SSE buffered.
Lors d’une reconnexion réseau, la lecture reprend au dernier curseur de transcription. Elle ne relance jamais l’agent et ne renvoie jamais un message automatiquement. Si le résultat d’un lancement est incertain, rouvrez Nouvelle session, choisissez la connexion et consultez Conversations distantes. Une nouvelle tentative explicite de la même demande conserve son identifiant d’intention. Ne changez pas cet identifiant pour contourner un résultat incertain.
Fermer une vue ne détache que son lecteur. Supprimer la connexion enregistrée retire l’accès à l’espace de travail et arrête les commandes qu’elle a lancées sur A, mais conserve la conversation sur B. Fermer la session arrête l’agent et ses commandes ; sa transcription reste sur B et peut être rouverte en lecture. Pour relancer un agent après la fermeture, créez une nouvelle session. Les conversations distantes enregistrées apparaissent sous leur connexion dans Nouvelle session. Ces règles décrivent le formulaire distinct de session distante ; Création d’application et Posez votre question conservent leur historique habituel et leur action Reprendre explicite sur A.
Portée, révocation et limites
Un token créé pour Une application donne uniquement accès à l’API IA pour le texte et les images. Pour Une autre instance, le choix par défaut autorise l’accès à l’espace de travail de l’instance qui démarre la session. Choisir Cette instance et un projet lors de la création du token autorise plutôt les sessions dans ce seul projet et uniquement les conversations créées avec ce token. Cela n’autorise ni la connexion du propriétaire, ni les paramètres, ni les autres conversations, ni les API de fichiers du propriétaire, ni la gestion des tokens. Les outils de l’agent respectent les autorisations choisies au lancement ; choisir un projet n’isole pas le processus au niveau du système d’exploitation.
L’accès aux espaces de travail connectés est accordé par défaut aux nouveaux tokens Une autre instance ; il n’est jamais ajouté automatiquement aux anciens tokens. Chaque lancement sur A associe une identité de répertoire de projet, une connexion, un mode d’autorisation et une intention. B ne dispose d’aucune API pour changer ce répertoire racine. Les chemins de fichiers sont relatifs, les liens symboliques vers l’extérieur sont refusés et une écriture exige le SHA-256 observé lors de la lecture. Les modifications ambiguës ou fondées sur un état périmé sont refusées. Les commandes shell approuvées disposent des droits de l’utilisateur sur A ; le répertoire du projet n’est pas une isolation système.
Chaque action sur l’espace de travail reçoit un reçu persistant. Une reconnexion ne peut renvoyer que la même identité d’opération et retrouve son résultat enregistré. Si le serveur s’arrête pendant une action, son résultat est déclaré incertain au lieu de la relancer. Les commandes lancées pour la session s’arrêtent lorsque le serveur A s’arrête, que l’accès est révoqué ou que la session se termine. Vérifiez les effets d’une commande incertaine avant d’en demander une nouvelle. Redémarrer l’une ou l’autre instance ne lance jamais un autre agent et ne renvoie jamais une demande.
Les tokens expirent après 90 jours. Révoquer le token ou désactiver la passerelle distante supprime son autorisation de session et arrête ses agents. L’expiration est vérifiée à chaque opération et par un contrôle périodique limité, y compris après redémarrage. Supprimer une connexion enregistrée ne révoque pas le token d’origine : révoquez-le sur l’instance qui l’a émis si vous voulez supprimer son autorisation.
La passerelle accepte au plus 20 sessions actives par instance émettrice, quatre lecteurs en direct par token et 20 lecteurs au total. Chaque Chat ouvert utilise un lecteur ; Terminal peut en ajouter un. Ce sont des limites locales de ressources, sans rapport avec les quotas d’abonnement. Les quotas des fournisseurs et les exigences de compatibilité restent applicables. Connecter les instances n’installe ni ne met à jour une CLI de fournisseur.
Les espaces de travail connectés ajoutent au plus 20 canaux d’outils persistants sur B, dont quatre par token. Chaque espace de travail accepte quatre appels en cours et quatre commandes de projet en cours. A accepte au plus 20 espaces de travail actifs et 20 commandes au total. Les trames sont limitées à 2 MiB, les fichiers ordinaires et résultats de modification à 8 MiB, les appels à 20 000 par espace de travail et les commandes à dix minutes. Les réponses des outils paginent les fichiers et signalent les fins de sortie de commande tronquées. Une page de lecture ne dépasse pas 128 KiB ; les arguments d’écriture et de modification ont leurs propres limites et doivent tenir dans une seule trame. Ces limites ne créent aucune nouvelle adresse publique.
API de session pour un serveur personnel ou un outil d’IA
Le schéma OpenAPI des sessions décrit le protocole. Envoyez Authorization: Bearer TOKEN à chaque appel vers l’URL de base /api/ai-gateway/v1 de l’instance émettrice. Refusez les redirections ; ne mettez pas d’identifiants dans l’URL ou ses paramètres. Ne placez pas le token dans une demande à l’agent, le code source ou le stockage du navigateur.
| Point d’accès | Fonction |
|---|---|
GET /session-catalog |
Version du protocole, projet autorisé facultatif, workspaceAccess et catalogue actuel des agents, modèles, efforts et autorisations |
POST /sessions |
Lance un agent natif ; UUID intention, engine, model, permissionMode et initialPrompt requis ; effort et name facultatifs |
GET /sessions |
Jusqu’à 100 conversations récentes créées avec ce token |
GET /sessions/{id} |
Résumé actuel d’une conversation créée avec ce token |
POST /sessions/{id}/resume |
Reprise explicite avec UUID intention, initialPrompt, et éventuellement model et effort ; même contexte natif sur B |
POST /sessions/{id}/events |
Flux SSE continu ; envoyez d’abord le JSON { "offset": null }, puis le dernier décalage reçu à la reconnexion |
POST /sessions/{id}/transcript |
Lit une page ; offset demande les événements récents, before les messages précédents |
POST /sessions/{id}/input |
Message suivant : UUID intention et text non vide |
POST /sessions/{id}/interaction |
UUID intention, id de la demande en cours et réponse acceptée : option, texte ou fermeture |
POST /sessions/{id}/command |
UUID intention et action : interrupt, close, model, effort ou permission ; les changements de choix exigent un arg du catalogue |
GET /sessions/{id}/terminal |
WebSocket authentifié ; sortie brute, saisie JSON { "t": "i", "d": "text" }, redimensionnement { "t": "r", "c": 100, "r": 30 } |
GET /workspaces/{id} |
Retrouve la session sur B par l’UUID initial de l’espace de travail ; ne créez pas de remplacement après une réponse perdue |
GET /workspaces/{id}/channel |
A ouvre un WebSocket authentifié vers B avant le lancement ; B envoie des appels d’outils et A renvoie leurs résultats |
Pour utiliser le pont intégré vers le projet de A, appelez l’API de A avec l’authentification du propriétaire depuis votre client. Enregistrez B avec POST /api/ai-gateway/connections, consultez les choix avec GET /api/engines?remoteConnectionId=ID&workspaceLocation=local, puis appelez POST /api/ai-gateway/connections/ID/sessions avec localProject: "P1" et les choix du catalogue. A fournit l’identité de l’espace de travail et le canal de retour. Le nom agremote_… retourné permet d’ouvrir /api/ai-gateway/remote-sessions/NAME/events et /interaction sur A, y compris pour les autorisations demandées à A. Le token de passerelle de B n’autorise pas ces points d’accès réservés au propriétaire.
Pour Création d’application et Posez votre question, utilisez l’appel habituel POST /api/sessions authentifié comme propriétaire sur A, avec remoteConnectionId: "SAVED_CONNECTION_ID" et les choix du catalogue de B. Utilisez askAnything: true pour une question ; pour un projet d’application existant, fournissez project et createMode: true. Pour créer d’abord une application, appelez POST /api/create-mode/projects/confirmed avec prompt, appName et projectName. Conservez le même idempotencyKey si le résultat du lancement est incertain. La réponse fournit les valeurs habituelles tmuxName, sessionId et remoteAgent de A. Les messages suivants, la transcription, l’espace de travail et la reprise utilisent les API de conversation normales de A. Le token de passerelle de B ne remplace jamais les identifiants du propriétaire sur A.
Si vous développez un serveur de connexion distinct, consultez le protocole de l’espace de travail et les schémas des outils. Ne lancez sur B avec workspace: { id: UUID, project: "P1" } qu’après avoir implémenté le canal, le répertoire racine fixé sur A, les autorisations et les reçus persistants des opérations. Un nom de projet seul ne donne pas accès aux fichiers. Pour ces sessions, la saisie dans le terminal de B et les changements natifs d’autorisation sont désactivés.
Un événement SSE update contient { id, project, session, transcript }. transcript.events est la nouvelle page d’événements ; offset est son curseur entier opaque ; interaction ou terminalAttention décrit une éventuelle demande de saisie. Reconstituez les trames SSE complètes lorsque le réseau les découpe. Ignorez les commentaires de maintien de connexion. Si un événement error arrive, conservez la conversation et reconnectez-vous pour lire la suite. Une réponse réseau perdue ne prouve jamais qu’une opération n’a pas eu lieu.
Les modes d’autorisation, identifiants de modèles et niveaux d’effort viennent de /session-catalog, pas des identifiants composites de l’API texte. La création et les actions refusent les champs non pris en charge. La demande initiale et les messages suivants sont limités à 64 KiB. Une intention de lancement appartient au token ; une intention d’action appartient à la session. Réutiliser une intention avec un contenu différent est refusé. Après une tentative de remise à l’agent, un résultat inconnu reste AI_GATEWAY_SESSION_UNCERTAIN et ne peut pas être relancé automatiquement.
En cas de quota atteint, attendez le rétablissement réel indiqué par le fournisseur ; une heure inconnue reste inconnue. Pour AI_GATEWAY_SESSION_ATTENTION, répondez à la demande en cours. Si l’accès est révoqué ou expiré, créez un nouveau token limité sur l’instance émettrice. Si un modèle manque, actualisez le catalogue. Un nouveau token ne peut pas reprendre des conversations du propriétaire déjà existantes.
Création d’apps et « Tout demander » sur A
L’API du propriétaire conserve les fichiers, l’historique et les aperçus sur A, tandis que la connexion enregistrée à B y exécute l’agent.
// 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' }.
Projet sur A, agent sur B
Utilisez les identifiants de session du propriétaire de A dans un service privé. Le token de passerelle de B est déjà enregistré sur A et ne peut pas remplacer ces identifiants. Choisissez les valeurs du catalogue et conservez un seul UUID d’intention de lancement.
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.
Session active sur le projet de B
Node.js 22 ou plus récent. Définissez AIRCODE_REMOTE_BASE_URL et AIRCODE_REMOTE_TOKEN dans la configuration de serveur privé. La première exécution imprime le catalogue jusqu’à ce que vous choisissiez également AIRCODE_REMOTE_ENGINE, AIRCODE_REMOTE_MODEL et AIRCODE_REMOTE_PERMISSION. AIRCODE_REMOTE_EFFORT est optionnel. Le lecteur se ferme après 30 secondes ; l’agent à distance reste en cours d’exécution.
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; }