语言简体中文

远程会议

将 URL 和另一个个人 AirCode Ø 实例的范围代币保存,然后在 新会话中选择该连接 → 在上运行。 远程实例呼吁其自己的连接 Claude Code, Codex 或其他可用的本地提供商。 您的当前客户端在远程代理工作时显示对话和终端。

选择项目居住的地方。 使用 项目位置 → 此实例,A 拥有项目 P1 和 B 使用 B 的提供商帐户运行代理。 B 通过工作空间工具请求所需的文件; A 读取、编辑和运行实际 P1 目录中的命令。 没有克隆或最终同步。 请求的代码和命令输出达到 B 及其 AI 提供商。 提供商凭证和使用会计留在B。 依赖,构建和测试保持在A。

A 打开对 B 的验证的 WSS 连接,该连接在两个方向上传递工具调用。 机器可以位于不同的网络或数据中心。 A 不需要入口端口或公共地址; B 需要一个可访问的 HTTPS 实例 URL,其代理必须支持 WebSocket 升级。 两种实例都必须支持连接的工作区。 关闭浏览器或移动视图将留下该服务器连接的位置。 必须继续运行,以便代理人使用其项目。

项目位置 → 远程实例 保留了另一个模式:代理人和代币的授权项目都在 B。 这两种模式是明确的;选择B上的类似名称项目并不意味着P1上的A。

连接您的两个实例

  1. 在远程实例中,连接您的本地提供商帐户 AI 使用。
  2. 打开 设置 → AI gateway,在手机上打开 更多 → AI gateway。在 创建访问权限 下开启 允许私人连接。在 创建令牌 中保留默认的 另一实例,为连接命名,然后选择 创建连接令牌。默认情况下,文件和命令留在 A,代理使用 B 的 AI 账户。若要处理 B 的文件,请打开 在哪里处理文件和运行命令?,选择 此实例 及 B 上的项目。每次会话选择 可在同一个 B 项目边界内使用两种位置。
  3. 复制其一次性披露的代币和显示的 Base URL。
  4. 在 A 上打开 设置 → AI gateway → 添加另一服务器。输入连接名称、B 的实例 URL 或 Base URL,以及令牌,然后选择 保存连接。也可以从 新会话 → 运行于 → 添加远程实例… 添加。
  5. 在 新会话中,选择在 上运行中保存的连接。 检查一下 项目位置 默认为 此实例允许代币时 。 保持或选择 P1 在 项目下,此实例中,选择 → 模型 → 努力 → 许可证,输入提示,然后选择 开始会话。 目前的工作区适配器支持 Claude Code 和 Codex;可用 选项来自B的现场目录。 其他发动机保持远程项目模式。

该代币被发送一次到您的当前服务器进行加密存储。 Web 和本地客户端随后使用连接标识符。 网站永远不会收到 URL 或代币。 使用 HTTPS 用于互联网连接或您信任的私人网络。 网关代币是个人身份证,不是共享链接。

工作区桥和其 MCP 工具只启动 项目位置 → 此实例 与远程代理。 通常的会话不会开始任何这些服务。 关闭最后一个分布式会话会阻止私人桥梁倾听器;仅关闭其浏览器或移动视图会让该会话运行。

Codex 目前需要本地文件支持登录 (auth.json) 在 B 这个模式;仅键入登录被明确拒绝。 其私人配置使用链接到原始帐户和对话文件,而不复制 OAuth 值。 CLI 兼容性和机器管理策略仍然适用。

使用远程代理工作

向应用程序提示并问任何问题

打开 A 上的任何一扇门,打开 模型 (编译器中的代理设置芯片),然后在下方选择保存的 B 连接 奔跑. 从 B 的目录中选择 → 模型 → 努力 → 许可证 。 您也可以使用 **添加远程实例…**保存连接。 两种实例都需要一个版本广告 conversationWorkspace: 1;缺少范围或不可用 B 块启动。

对于 向应用程序的提示,请描述应用程序,并如往常确认其应用程序/项目名称。 项目、保存的应用程序、开发主管和预览属于A。 后续消息、视觉编辑和选择的屏幕截图区域将转到B代理。 计划首先 →单个代理 可以提交其 Plan 检查点 A; 汇聚目前需要 此实例。

对于 问任何事,输入您的问题和任何文件。 其私人对话文件夹和 文件 库存属于 A。 附加的图像和PDF可以通过工作区工具读取。 转换为项目 将 A 文件夹复制为普通 A 项目。

这些门保持普通的对话身份和历史。 打开结束的对话并选择 简历 明确恢复了 B 的同一本地背景;重新连接网络只会重新连接读者。 附件、命令输出和私人增量转录在 A 上仍然可用。 代理设置可以更改 A 的工作空间权限和 B 的模型/努力。 终端显示 B 并且只读;命令在 A 上运行并显示在 Chat 中。 浏览器、规划和 Knowledge 工具通过对话的现有 A 功能,可根据需求进行曝光。 没有助手或桥梁通过此功能添加到普通的本地对话中。

聊天 显示新的转录事件和当前的权限/问题对话。 发送后续消息,回答对话,或使用 停止 来中断当前的响应。 在连接工作区模式中,权限由A执行:只读模式拒绝写作和命令;正常模式在这个对话中要求;编辑批准模式允许文件编辑,但要求壳命令;绕过允许所请求的操作。 单独的新会话远程工作区表单在启动时修复允许模式;上面两个对话门暴露了A的正常允许控制。 代理终端 (仅可读) 显示 B 的实际供应商输出;项目命令通过工作区工具在 A 上运行,并在聊天中流出其输出。 在远程项目模式中, 终端 也接受本地输入和调整大小。

终端通过WebSocket传输实时输出。 聊天使用连续的 SSE 响应,并在服务提供商编写其转录时发布事件;一些服务提供商记录一个完成的消息,而不是每个代币。 这并不保证每个 CLI的代币级别聊天更新。 单独的单独文本的 /chat/completions API 仍然使用 缓冲 SSE。

网络重新连接从最后一个转录方程式中读取。 它从来没有自动启动另一个代理或重新发送消息。 经过不确定启动后,重新打开 新会话,选择连接,然后在 远程对话下查看。 同一请求的明确重复保持其意图ID。 不要更改ID,仅仅是为了绕过不确定的结果。

关闭视图只会让其读者分离。 删除已保存的连接会关闭其工作区访问,并停止其在 A 上拥有的命令;它会保留 B 的对话。 闭合会话 停止代理和其所有命令;其转录仍然保留在远程实例中,可重新打开读取。 启动一个新的会话,在关闭后再次运行代理。 已保存的远程对话在新会话中列出他们的连接。 本段落描述了单独的新会话远程表单。 请求应用程序和问任何事,保持其正常的历史和明确的复制行动在A。

范围、撤销和限制

为 应用 创建的令牌仅允许通过 AI API 处理文本和图像。选择 另一实例 时,默认授予对启动会话的实例上工作区的访问权限。创建令牌时选择 此实例 和一个项目,则仅允许在该项目中启动会话,并访问使用该令牌创建的对话。它不授予所有者登录、设置、其他对话、任意所有者文件 API 或令牌管理权限。代理工具使用启动时选择的权限;选择项目不等于操作系统级隔离。

新建的 另一实例 令牌默认包含对连接工作区的访问权限;旧令牌不会自动获得此权限。 每个在 A 上发射将一个项目目录身份、连接、权限模式和意图联系起来。 B 没有 API 来改变这个根。 文件路径是相对的,拒绝了外部符号链接,并且需要在读取文件时观察到的 SHA-256。 永久性或模糊的编辑被拒绝。 批准的壳命令具有A的用户权限;项目 cwd不是操作系统沙盒。

工作空间效应具有持久的收入。 重新连接只重置相同的操作身份,并返回其记录的结果。 在某个效果中发生服务器故障时,报告不确定性,而不是重新启动它。 当 A 的服务器退出,访问被撤销或会话关闭时,所有命令会停止。 在请求新命令之前,检查不确定命令的效果。 重新启动任何一个实例永远不会启动另一个提供商或重新启动提示。

代币在90天后到期。 撤销代币或禁用远程网关会删除其会话权限并停止其代理。 每一次操作和边缘背景检查都检查到过期,包括服务器重新启动后。 删除已保存的连接不会取消原始代币:如果您想要取消权限,则在发行实例中取消该代币。

每个发行实例最多有 20 个活跃的网关会话和每条代币的 4 个直播读者,在网关上有 20 个读者。 每个打开的聊天使用一个读者;打开的终端可以添加另一个。 这些是本地资源限制,而不是订阅配额。 供应商配额和兼容性要求仍然适用;没有提供商 CLI 通过连接安装或更新。

连接的工作区在 B 上添加最多 20 个持久工具渠道,每代币 4 个,每工作区有 4 个飞行调用和 4 个运行项目命令。 A 允许 20 个活跃的工作区和 20 个命令。 框架限制在 2 MiB,常规文件 / 编辑结果在 8 MiB,呼叫到每工作空间20,000,命令到十分钟。 工具响应页面文件内容,并明确标记剪切的命令尾巴。 阅读页面最多为 128 KiB;写 / 补丁文本参数有自己的方案限制,必须在一个框架内放置。 这些是本地资源的局限性;没有创建额外的公共来源。

会话 API 用于个人后端或 AI 工具

会话 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 连续 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 和 action: interrupt, close, model, effort 或 permission;选择更改需要 arg 从目录
GET /sessions/{id}/terminal 身份验证 WebSocket 升级;原始终端输出, JSON 输入 { "t": "i", "d": "text" }, 更大 { "t": "r", "c": 100, "r": 30 }
GET /workspaces/{id} 通过原始工作区 UUID恢复 B 会话;从未在丢失的响应上创建替换
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,包括 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的正常对话API;永远不要替代A的所有者凭证的网关代码。

如果要实现单独的连接服务器,请参阅 工作区协议和工具方案。 在 workspace: { id: UUID, project: "P1" } 上运行 B 时,只有在实现频道、粘贴本地根、权限和持久操作收据后才能启动。 标签本身不提供文件访问权限。 B 的终端输入和原生权限更改在这些会话中被禁用。

一个 SSE update 包含 { id, project, session, transcript }。 transcript.events 是新事件页面, offset 是其不透明整数路径, interaction 或 terminalAttention 描述当前输入需求。 保持完整的 SSE 框架在网络碎片上。 忽略评论 保守 在 error 事件中,保留对话并重新连接以便阅读。 永远不要推断一个操作并非来自失去的网络响应。

许可模式、模型ID和努力来自 /session-catalog,而不是文本 API的复合模型ID。 会话创建和操作拒绝不受支持的字段。 初始提示和后续内容仅限于64 KiB。 发射意图被分为代币;行动意图被分为会话。 不同内容的意图重复使用被拒绝。 一旦尝试运行时交付,未知的结果仍然是 AI_GATEWAY_SESSION_UNCERTAIN ,不能自动重播。

对于配额墙,等待供应商的实际恢复;未知的重置时间仍然未知。 对于 AI_GATEWAY_SESSION_ATTENTION,回答当前对话框。 对于被撤销/过期访问,在发行实例上创建一个新的范围代币。 缺少的模型,更新目录。 现有所有者对话不能通过新代币。

请点击 app 并在 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的所有者会话凭证。 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.

在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; }