リモートセッション
URL と別の個人 AirCode Ø インスタンスからスケープトークンを保存し、 New session → Run onでその接続を選択します。 リモートインスタンスは、独自の接続 Claude Code、 Codex 、または他の利用可能なネイティブプロバイダーを呼び出します。 現在のクライアントは、リモートエージェントが動作するように会話とターミナルを表示します。
プロジェクトが住む場所を選択します。 プロジェクトの場所 → このインスタンスでは、A はプロジェクト P1 を所有し、B は B のプロバイダー アカウントを使用してエージェントを実行します。 B は、ワークスペース ツールを通じて必要なファイルをリクエストします; A は、実際の P1 ディレクトリでコマンドを読み、編集、実行します。 クローンや最終同期はありません。 リクエストされたコードとコマンドの出力は、B とその AI プロバイダーにまで届きます。 利用者証明書と利用会計は、Bに留まります。 依存性、構造、テストはAに留まります。
A は B への認証済み WSS 接続を開き、両方向でツール呼び出しを実行します。 機械は、異なるネットワークやデータセンターに存在する可能性があります。 A は、入力ポートまたは公開アドレスを必要としません; B は、アクセス可能な HTTPS インスタンス URLを必要とし、そのプロキシは WebSocket のアップグレードをサポートする必要があります。 両方のインスタンスは、接続されたワークスペースをサポートする必要があります。 ブラウザまたはモバイルビューを閉じると、このサーバー接続が置かれています。 エージェントがそのプロジェクトを使用するために動き続けなければなりません。
プロジェクトの場所 → リモートインスタンス は、他のモードを保持します: エージェントとトークンの承認されたプロジェクトの両方が B にあります。 これらの2つのモードは明示的なものであり、Bで同様に名付けられたプロジェクトを選択することは、AでP1を意味しない。
2 つのインスタンスを接続する
- リモートインスタンスでは、Native Provider アカウントを AI Usageに接続します。
- 設定 → AI gateway、モバイルでは その他 → AI gateway を開きます。アクセスを作成 で プライベート接続を許可 を有効にします。トークンを作成 では 別のインスタンス を選んだまま接続名を入力し、接続用トークンを作成 を選択します。初期設定ではファイルとコマンドはAに残り、エージェントはBのAIアカウントを使います。Bのファイルで作業するには ファイルとコマンドを扱う場所 で このインスタンス とB上のプロジェクトを選びます。セッションごとに選択 なら両方を使えます。
- Base URLからトークンをコピーします。
- Aで 設定 → AI gateway → 別のサーバーを追加 を開きます。接続名、BのインスタンスURLまたはBase URL、トークンを入力し、接続を保存 を選択します。新しいセッション → 実行先 → リモートインスタンスを追加… からも追加できます。
- New sessionでは、 Run onで保存された接続を選択します。 チェック THE プロジェクト位置 デフォルト 2 この裁判所 トークンが許す時 P1 を選択するか、P1 を選択するか プロジェクト この例選択する ハーネス モデル → 努力 → 許可を指定し、プロンプトを入力し、 セッション開始を選択します。 現在のワークスペース・アダプターは Claude Code と Codexをサポートしています。 選択は B のライブカタログから入手できます。 他のエンジンは、リモートプロジェクトモードを保持します。
トークンは一度、暗号化されたストレージのために現在のサーバーに送信されます。 Web およびネイティブ クライアントは、接続識別子を使用します。 ウェブサイトは URL またはトークンを決して受け取らない。 HTTPS を使用して、インターネット接続または信頼できるプライベート ネットワークを使用します。 ゲートウェイ・トークンは、個人的な認証であり、リンクを共有しない。
ワークスペース ブリッジと MCP ツールは、 プロジェクト ロケーション → このインスタンス をリモート エージェントで起動します。 通常のセッションは、これらのサービスのいずれも開始しません。 最後の分散セッションを閉じると、プライベート ブリッジ オーディエントが停止しますが、ブラウザまたはモバイル ビューのみを閉じると、セッションが実行されます。
Codex は現在、このモードの B でネイティブ ファイル バックアップ ログイン (auth.json) を必要としています; keyring-only ログインは明示的に拒否されます。 プライベート構成では、ネイティブアカウントと会話ファイルへのリンクを使用し、OAuth 値をコピーすることなく使用します。 CLI 互換性および機械管理ポリシーは引き続き適用されます。
リモートエージェントで作業
アプリへのプロンプトと何かを尋ねる
A ドアを開けて A ドアを開けて A ドアを開けて A ドアを開けて A モデル (composer の Agent Settings チップ) で、その後、保存された 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 フォルダーを普通の A プロジェクトにコピーします。
これらのドアは、普通の会話のアイデンティティと歴史を保持します。 Resume を選択して終了した会話を開いて、B で同じネイティブ コンテキストを明示的に再開します。 添付ファイル、コマンド出力、プライベート増加式トランスクリプトは、A で利用できます。 エージェント設定は、Aのワークスペース許可およびBのモデル/努力を変更することができます。 ターミナルは B を表示し、読み込みのみで、コマンドは A で実行され、チャットに表示されます。 ブラウザ、プランニング、 Knowledge ツールは、会話の既存のA機能を通じて需要に応じて表示されます。 この機能により、通常のローカル会話に助手やブリッジが追加されません。
Chat は、新しいトランスクリプトイベントと現在の許可/質問ダイアログを表示します。 フォローアップメッセージを送信し、ダイアログに回答するか、 停止 を使用して、現在の応答を中断します。 Connected-workspace モードでは、A によって許可が実施されます: 読むだけのモードは書き込みとコマンドを拒否します; 通常のモードはこの会話で要求します; 編集承認モードはファイルの編集を許可しますが、シェルコマンドを要求します; バイパスは要求された操作を許可します。 別個の New session remote workspace フォームは、起動時に許可モードを修正し、上記の 2 つの会話ドアは A の通常の許可制御を露出します。 エージェント ターミナル (読み込みのみ) は、B の実際のプロバイダー 出力を表示し、プロジェクト コマンドはワークスペース ツールを介して A で実行し、その出力を Chat でストリーミングします。 リモートプロジェクトモードでは、 ターミナル もネイティブ インポートとリサイズを受け入れます。
ターミナルはWebSocketを通じてライブ出力を送信します。 チャットは継続的な SSE 応答を使用し、プロバイダーがそのトランシップを書くようにイベントを公開します。 これは、すべての CLIのためのトークンレベルのチャット更新を約束しません。 別々のテキストのみ /chat/completions API まだ使用 バッファ SSE.
ネットワーク再接続は、最後のトランスクリプトカーサーから読み取ります。 別のエージェントを起動したり、自動的にメッセージを再送信したりすることはありません。 不確実な起動後、 New sessionを再開し、接続を選択し、 Remote conversationsを参照してください。 同じ要求の明示的なリリースは、その意図のIDを保持します。 不確実な結果を回避するためにIDを変更しないでください。
視点を閉じると、読者だけが解放されます。 保存された接続を削除すると、そのワークスペースへのアクセスが閉じられ、A に所有するコマンドが停止され、B の会話が保持されます。 **** セッション閉じは、エージェントとその所有コマンドを停止し、そのトランスクリプトはリモートインスタンスに残っており、読み返すために再開できます。 新しいセッションを開始して、エージェントを閉じた後に再度実行します。 保存されたリモート会話は、新しいセッションの接続の下にリストされます。 この段落では、別個の新しいセッションのリモートフォームを説明します。 アプリにプロンプトし、何でも尋ねると、定期的な履歴と明示的なリサイクルアクションをAに保持します。
範囲、撤回および制限
アプリ 用のトークンは、テキストと画像の AI API へのアクセスだけを許可します。別のインスタンス では、既定でセッションを開始するインスタンスのワークスペースへのアクセスを許可します。トークン作成時に このインスタンス とプロジェクトを選ぶと、そのプロジェクトでのセッションと、そのトークンで作成した会話だけにアクセスできます。所有者のログイン、設定、他の会話、所有者の任意のファイル API、トークン管理は許可されません。エージェントのツールには開始時に選んだ権限が適用されます。プロジェクトの指定は OS レベルの隔離ではありません。
接続元のワークスペースへのアクセスは、新しい 別のインスタンス 用トークンでは既定で許可されますが、既存のトークンに自動追加されることはありません。 A での各リリースは、1 つのプロジェクトディレクトリのアイデンティティ、接続、許可モード、および意図を結びつける。 B には、そのルートを変更する API がありません。 ファイルパスは相対的で、外部のシムリンクは拒否され、書き込みはファイルを読み取るときに観察された SHA-256 を必要とします。 固定または曖昧な編集は拒否されます。 承認されたシェルコマンドには、Aのユーザー権限があり、プロジェクト cwd は OS サンドボックスではありません。
ワークスペース効果には持続可能な収入があります。 再接続は、同じ操作アイデンティティを再接続し、記録された結果を返します。 エフェクト中にサーバーが故障した場合、再起動する代わりに不確実性が報告されます。 所有コマンドは、Aのサーバーが終了したとき、アクセスが撤回されたとき、またはセッションが終了したときに停止します。 新しいコマンドを要求する前に、不確実なコマンドの効果を確認します。 どちらのインスタンスを再起動すると、別のプロバイダーを起動したり、プロンプトを再起動したりすることはありません。
トークンは90日後に有効期限が切れる。 トークンを取り消すか、リモートゲートウェイを無効にすると、セッション権限を削除し、エージェントを停止します。 Expiry は、サーバーの再起動後も含めて、各操作および限られたバックグラウンドチェックによってチェックされます。 保存された接続を削除すると、元のトークンを取り消すことはありません:権限を取り消したい場合は、発行インスタンスでそれを取り消します。
発行インスタンスごとに最大20のアクティブゲートウェイセッションと、トークンごとに4つのライブリーダーがあり、ゲートウェイのあたりに20のリーダーがいる。 開いたチャットは1つのリーダーを使用し、開いたターミナルは別のリーダーを追加することができます。 これらは地元のリソース制限であり、サブスクリプション認可ではありません。 プロバイダーの割合および互換性要件は依然として適用され、プロバイダー CLI は接続によってインストールまたは更新されていません。
接続されたワークスペースは、B で最大 20 個の持続的なツールチャンネルと 1 つのトークンあたり 4 個のツールチャンネルを追加し、ワークスペースあたり 4 件のインフラ通話と 4 件のプロジェクトコマンドを実行します。 A は 20 つのアクティブなワークスペースと 20 つのコマンドを可能にします。 フレームは 2 MiB に制限され、通常のファイル/編集結果は 8 MiB に制限され、ワークスペースあたりの呼び出しは 20,000 に制限され、コマンドは 10 分に制限されます。 ツールはページファイルのコンテンツに対応し、断片されたコマンドの尾を明示的にマークします。 読むページは最大 128 KiB で、書き込み/パッチ テキスト アルゴリズムには独自のスケジュール制限があり、1 つのフレームに適合する必要があります。 これらは地元のリソースの限界であり、追加の公共の起源は作成されません。
Session API for a personal backend or AI tool
セッション OpenAPI スキーマ は、このプロトコルを説明します。 発行インスタンスの /api/ai-gateway/v1 Base URLへの各呼び出しに Authorization: Bearer TOKEN を使用します。 リダイレクトを拒否し、決して URL/query 資格を使用しないでください。 トークンはプロンプト、ソースコード、ブラウザストレージに置かないでください。
| エンドポイント | 意味 |
|---|---|
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 |
フォローアップ: UUID intention と非空の text |
POST /sessions/{id}/interaction |
UUID intention、現在のダイアログ id、およびサポートされているオプション/テキスト/拒否回答 |
POST /sessions/{id}/command |
UUID intention and action: interrupt, close, model, effort or permission; 選択の変更はカタログから arg を必要とする |
GET /sessions/{id}/terminal |
Authenticated WebSocket upgrade; raw terminal output, JSON input { "t": "i", "d": "text" }, サイズ { "t": "r", "c": 100, "r": 30 } |
GET /workspaces/{id} |
オリジナルワークスペース UUIDで B のセッションを回復する; Never create a replacement on a lost response |
GET /workspaces/{id}/channel |
A は、起動前に認証済みの WebSocket を B に開きます; B は、ワークスペース ツールの呼び出しを送信し、A は結果を返します。 |
組み込みの A ワークスペース ブリッジの場合、現在のクライアントから A の所有者認証 API を使用して、B を POST /api/ai-gateway/connectionsで保存し、 GET /api/engines?remoteConnectionId=ID&workspaceLocation=localで選択肢を発見し、 POST /api/ai-gateway/connections/ID/sessions を localProject: "P1" とカタログ選択に呼び出します。 A は、ワークスペースのアイデンティティと逆チャンネルを提供します。 返された agremote_… 名は A の /api/ai-gateway/remote-sessions/NAME/events と /interactionをオープンします。 Bのゲートウェイトークンは、これらの所有者のエンドポイントを許可することはできません。 次の例は、この完全な区別を示しています。
ゆえに 早速 app そして 何でも尋ねる通常のオーナー認証を使用する POST /api/sessions A は with 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の通常の会話APIを使用し、Aの所有者の認証のゲートウェイトークンを置き換えることはありません。
別個の接続サーバーを実装する場合は、 ワークスペース プロトコルとツール スケジュールを参照してください。 workspace: { id: UUID, project: "P1" } で B を起動するには、チャンネル、固定ローカル ルート、許可、および持続可能な運用受信を実装した後のみです。 ラベルだけでは、ファイルへのアクセスを提供しません。 B の端末入力とネイティブ許可の変更は、これらのセッションで無効になります。
SSE update は { id, project, session, transcript }を含みます。 transcript.events は新しいイベントページであり、 offset はその不透明な整数コースorであり、 interaction または terminalAttention は現在の入力ニーズを説明します。 SSE フレームをネットワーク ブロックで完全に保持します。 コメンテーターの無視 error イベントでは、会話を保持し、読み取りのために再接続します。 決して、操作がネットワークの反応を失ったことから起こらなかったことを推測しないでください。
許可モード、モデルID、および努力は、 /session-catalogから来ていますが、 APIのコンポジット モデル ID ではありません。 セッション作成とアクションは、サポートされていないフィールドを拒否します。 最初のプロンプトとフォローアップは64 KiBに制限されます。 Launch intentions are scopeed to the token; action intentions are scopeed to a 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' }.
Project on A エージェント on B
プライベートバックエンドでAのオーナーのセッション認証を使用します。 BのゲートウェイトークンはすでにAに保存されており、この所有者認証を置き換えることはできません。 カタログ値を選択し、1 つのスタート意図 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.
Bのプロジェクトでのライブセッション
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; }