@ai-workbench/remote-agent 0.8.16 → 0.8.17
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -5
- package/dist/acp.js +1 -1
- package/dist/claude-local.js +2 -0
- package/dist/connect-contract.d.ts +5 -4
- package/dist/connect-need.d.ts +12 -0
- package/dist/connect-need.js +34 -0
- package/dist/employee-location.js +2 -1
- package/dist/employee-provider.js +4 -3
- package/dist/host-workspace.d.ts +13 -0
- package/dist/host-workspace.js +110 -0
- package/dist/index.js +14 -1
- package/dist/presentation.js +1 -0
- package/dist/provider.d.ts +2 -1
- package/dist/provider.js +124 -71
- package/dist/remote.d.ts +2 -0
- package/dist/remote.js +1 -0
- package/dist/workspace-source.js +2 -1
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
**`@ai-workbench/remote-agent@0.8.
|
|
1
|
+
**`@ai-workbench/remote-agent@0.8.17` 发布形态包。Claude、Codex 接入 L0.7.20 已选择的主机文件夹;成功核验来源和目录后才打开技术详情与派发,每次派发前重新核验。L 的具体原因和下一步动作直接给员工。包含0.8.16首次关闭、运行、诊断保存和清理结果留痕;历史原始撤销字段与61秒慢查询根因仍未查明。本版 leo 主机位置/真实员工链路尚未复验。通过官方插件 → 添加插件安装;固定包由编排发布,X2 不 publish。**
|
|
2
2
|
|
|
3
3
|
# 远程ACP插件
|
|
4
4
|
|
|
5
5
|
一个插件提供 **Claude(远程主机)** 和 **Codex(远程主机)**,通过员工主机上的官方CLI运行。Codex保留单次模式;Claude固定会话模式。只用官方模型列表、插件管理和配置入口,不提供自制面板、问答或登录按钮。登录和原生工作区绑定唯一归连接插件L。
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
当前版本0.8.17,由X2按编排裁定基于0.8.16交付主机来源执行和L具体Need透传;包名采用`@ai-workbench/remote-agent`以涵盖两个品牌;本轮按编排放行准备发布形态包,供 leo 真机自验;X 不执行实际发布,由编排统一发布。开发fixture逐字节保留L0.7.20固定包,公开类型与SSH导出从该包消费。运行时仍要求L的`getContractInfo()`同时返回API0.7.0、implemented、native-local-v1;旧L包、draft.3或只有版本号的不完整实现会明确报缺项。依赖范围为`~0.7.20`,L由官方插件管理单独安装,不打进X。组件验证不代表真实开通、模型或leo E通过;HanaMesh另验。
|
|
8
8
|
|
|
9
9
|
已包含L0.7.18消费增量:来源确认、输入接纳、登录、本机接纳、输入准备、模型目录及运行时解析的早期失败,用正式`preflightDiagnostics:1` / `preflight-check`进入L「连接前技术详情(含登录与工作区确认)」。阶段、真实来源/状态和错误代码可展开;未知来源为null,旧login-check的完整安全原因保留。记录不选择来源、不接纳输入、不自动重发;无原生会话的菜单查询仍在模型设置报告。当前工程和设置组件验证不等于leo GUI验收。
|
|
10
10
|
|
|
@@ -12,7 +12,11 @@
|
|
|
12
12
|
|
|
13
13
|
## 员工路径
|
|
14
14
|
|
|
15
|
-
从官方插件管理安装,照常使用默认工作区/会话/模型入口。L0.7.
|
|
15
|
+
从官方插件管理安装,照常使用默认工作区/会话/模型入口。L沿唯一“远程主机连接”设置节记录文件来源;L0.7.20新选主机位置由两品牌按已核验的规范目录执行,旧legacy主机绑定仍沿原规则;本地来源接入N0.4.4/S0.4.11员工端点。旧映射可在本轮官方一次确认中选择改用本机文件夹;拒绝、取消或输入/来源变化时不切换、不执行。也可在L设置逐工作区确认沿用原主机关联。缺正式host解析能力时明确说明所缺组件、保留原话,本轮不派发。本地六类工具经Claude公开toolAliases路由到员工电脑:cwd是员工选定目录,Git使用本机仓库,不回退主机同名目录。本地真实员工输入先取得官方当前轮次CAS预期,再调用L0.7.17 acceptLocalWorkspaceUserTurn;核回包后才打开D19和本轮主机目录/执行。每轮localAcceptanceId冻结进原详情sink,迟到结果不借新轮ID;LOCAL_USER_TURN_CHANGED立即停止本轮,不重接纳或退legacy。已用固定真包完成本地D19组件联测;编排已回证0.8.14 Claude/Opus5/Default在leo本地目录首轮写文件成功;该回证不覆盖本0.8.17或Codex,产品验收以各版本leo实际结果为准。本地文件不自动上传,X不猜目录、不选首项。首次说明使用官方普通回复;先经官方会话提交、持久化并读回核对,才向L确认。持久记录不等于员工已看见,GUI另验;日志丢失后的未知claim由L保留,不宣称端到端恰好一次。
|
|
16
|
+
|
|
17
|
+
新主机位置允许当前SSH员工账号可访问的已选绝对目录、文件系统根目录、非Git或只读目录;不改写原projectRoot、不创建Git、不转本机同名文件夹。read/execute必须可核实,write=false不额外一刀切拒绝,实际写入服从主机系统权限。运行前必核来源UUID、连接代次、目录及链接入口dev/ino和权限;revision与内容增删导致的mtime/ctime变化不单独失效。此核对属于正确性条件,不可关闭。最后inspect到主机spawn之间仍有OS时序边界,不表示原子目录句柄租约。
|
|
18
|
+
|
|
19
|
+
文件来源或位置改变后,原会话不能改绑或重放,员工看到“文件来源已改,请新建会话继续”;原会话历史仍可查。L的具体Need保留message/actor/action和代码,员工正文显示原原因与实际文字动作;明确选择文件来源的动作给“设置 → 远程主机连接 → 工作区文件来源”,新建会话等动作保留原标签,不按错误码猜导航;不可用动作明确标记。技术原因officialReason不进正文,未提供按钮API时不编造可点击入口。
|
|
16
20
|
|
|
17
21
|
未登录时两个模型均回复:**需要登录,请到设置 → 远程主机连接 点登录**。无L或缺正式能力时直接说明缺项,不让员工填写主机、用户、路径或密钥。
|
|
18
22
|
|
|
@@ -64,7 +68,7 @@ D19(0.8.1):相邻正文增量合成连续文本块,工作区首次说明
|
|
|
64
68
|
技术详情由 L 0.7.11 的唯一“远程主机连接”设置节展示,正文只放模型文本。命令目录保留完整描述;未知事件和工具输入输出保留字段、JSON类型及处理结果,未获准原文明确脱敏,数字用量可核对。没有 `remoteConversationDetails:1` 时明确提示“技术详情暂不可用”,本轮不发送模型请求。X 在官方当前 profile 的 `ai-workbench-remote-agent/diagnostics/` 保存私有诊断日志(目录700、文件600、落盘同步);写入/归属/顺序/容量失败中止本轮,不自动重发、换代重投、过期或淘汰。结果未知时只读核对原事件完整回执,未找到仍保持未知。X本轮无新增设置/阈值;L的单事件容量和分页设置在L设置节可见。
|
|
65
69
|
|
|
66
70
|
|
|
67
|
-
0.8.2 登录检查原交付需要 L `~0.7.11`(本统一版peer为`~0.7.
|
|
71
|
+
0.8.2 登录检查原交付需要 L `~0.7.11`(本统一版peer为`~0.7.20`),失败来源及固定分支保留到 L“登录前技术详情”(`loginDiagnostics:1`),员工正文保持原文。按 L 明确的本品牌 queued/checking/authorizing 状态订阅等待;不以全局checking猜测在途,不新增刷新、轮询、超时或重试。每轮ensureReady仍是合同规定的显式检查,L的新可信结果可以替代旧检查,较早返回不得覆盖它。
|
|
68
72
|
|
|
69
73
|
L安全规范的officialReason原串完整保留;旧/不支持格式明确记OFFICIAL_REASON_UNSUPPORTED与字节数/SHA,秘密原文不保存。L0.7.11的LOGIN_INSTANCE_MISMATCH记录通过observedLoginInstanceId与原始login.instanceId保存实际观测ID,事件及connection仍绑定当前可信ID/代次;L技术详情和X私有日志均保留真实双ID,不归一、不清空快照。复合未知官方原因以固定summary完整保存OFFICIAL_REASON_UNSUPPORTED及bytes/SHA,不交原文。X私有原快照先同步落盘,再追加L;ACK未知只读核实原事件,跨代不重写/重发。此版本的工程验证不替代leo员工路径复验,真实故障根因尚未证实。
|
|
70
74
|
|
|
@@ -76,7 +80,7 @@ D21(0.8.5):两个品牌的官方模型菜单从 L 的 `modelCatalogTarget:
|
|
|
76
80
|
|
|
77
81
|
每轮先核对目录代次和所选模型,再经 ACP 逐项配置模型及推理等级,收到完整配置确认后才发送输入。Claude 别名每轮须有精确的模型配置成功回执,并由同一员工主机、同一连接代际的目录唯一映射到具体型号。具体型号注明来自主机目录,会话未单独回报;旧恢复记录不作本轮证据。目录缺项、映射不唯一、回执不符或身份/代际变化时不发送输入。所选值、远端确认值及型号来源仅在恢复标识与技术详情保留,插件不向真实回复追加技术参数,确认配置不等于验证推理效果。连接/身份/目录变化清除旧确认;远端清理待定或结果未知时阻止派发,不自动清理重试或另开替代会话。Codex 单次模式当前不能确认此选择,会在发送前明确拒绝。新增设置 0;本轮工程证据、真实只读链路与产品未验项见 `docs/evidence/2026-10-22-X2-D21/`。
|
|
78
82
|
|
|
79
|
-
0.8.5统一包原依赖L `~0.7.13`;当前0.8.
|
|
83
|
+
0.8.5统一包原依赖L `~0.7.13`;0.8.16原对齐L `~0.7.19`;当前0.8.17对齐L `~0.7.20`。D8模型目录双事件长期监听保留;同epoch的recovery提示/失败消息变化不触发执行状态失效,真实实例/epoch/phase/可信目标/登录变化仍失效。完整D9两品牌正文/状态/就绪门呈现按编排00:50允许的范围记下一版,本版不宣称已消费完成;L的connectionMessages.administratorAfterMs仍只由L管理,X新增设置0。
|
|
80
84
|
|
|
81
85
|
D18(0.8.6):Codex 需要主机自有 adapter `2.2.2-aw.1` 的明确新建会话上下文政策,官方 CLI 仍为未改的0.160.1。系统/开发者要求仅在新建时生效;恢复时与初建摘要相同则正常恢复且省略新建专用字段,不声称恢复接口会更新指令。要求不同或清空时,在同一真实回复说明“会话要求已变化,已新建 Codex 会话继续处理,原会话仍保留。”,并新建远端承接公开对话,保留旧标识和查询记录;旧/非法初建摘要明确拒绝,不能猜测。
|
|
82
86
|
|
package/dist/acp.js
CHANGED
|
@@ -390,7 +390,7 @@ export class AcpSession {
|
|
|
390
390
|
signal?.addEventListener('abort', abort, { once: true });
|
|
391
391
|
try {
|
|
392
392
|
const hello = await this.stage('acp-initialize', () => this.connection.agent.request(acp.methods.agent.initialize, {
|
|
393
|
-
protocolVersion: acp.PROTOCOL_VERSION, clientInfo: { name: '@ai-workbench/remote-agent', version: '0.8.
|
|
393
|
+
protocolVersion: acp.PROTOCOL_VERSION, clientInfo: { name: '@ai-workbench/remote-agent', version: '0.8.17' },
|
|
394
394
|
clientCapabilities: { fs: { readTextFile: false, writeTextFile: false }, terminal: false, session: { notices: {} }, ...(this.employeeMeta ? { _meta: { awEmployeeExecution: { version: 1 } } } : {}) },
|
|
395
395
|
}, { cancellationSignal: signal }));
|
|
396
396
|
this.hooks.broker?.acknowledge(acp.methods.agent.initialize);
|
package/dist/claude-local.js
CHANGED
|
@@ -31,6 +31,8 @@ export function createClaudeLocalProvider(options) {
|
|
|
31
31
|
if (typeof local.getWorkspaceLocationState !== 'function')
|
|
32
32
|
throw failure('EMPLOYEE_CONTEXT_UNAVAILABLE');
|
|
33
33
|
const workspace = await options.readWorkspace(request.sessionId), state = local.getWorkspaceLocationState(), matches = state.items.filter(row => row.workspaceId === workspace.workspaceId);
|
|
34
|
+
if (matches.length === 1 && matches[0].source === 'host')
|
|
35
|
+
return; // Host state/Need belongs to the host resolver, before local admission.
|
|
34
36
|
if (matches.length !== 1 || state.phase !== 'ready' || state.instanceId !== connection.instanceId || state.epoch !== connection.epoch)
|
|
35
37
|
throw failure('EMPLOYEE_CONTEXT_CHANGED');
|
|
36
38
|
const item = matches[0];
|
|
@@ -2,8 +2,8 @@ import type { ConnectNeed } from '@ai-workbench/connect/api';
|
|
|
2
2
|
import type { ConnectTarget } from './provider.ts';
|
|
3
3
|
import type { ContractInfo, Provider, WorkspaceUse, WorkspaceNoticeReceipt as LWorkspaceNoticeReceipt } from '@ai-workbench/connect/api';
|
|
4
4
|
/** 正式L0.7公开类型;消费子集不表示已安装或已通过真实产品门。 */
|
|
5
|
-
export type { WorkspaceNotice, SessionProjectTarget } from '@ai-workbench/connect/api';
|
|
6
|
-
import type { SessionProjectTarget } from '@ai-workbench/connect/api';
|
|
5
|
+
export type { WorkspaceNotice, SessionProjectTarget, HostWorkspaceResolveContext, HostSessionProjectTarget } from '@ai-workbench/connect/api';
|
|
6
|
+
import type { SessionProjectTarget, HostWorkspaceResolveContext, HostSessionProjectTarget } from '@ai-workbench/connect/api';
|
|
7
7
|
export type LoginProvider = Provider;
|
|
8
8
|
export type WorkspaceNoticeReceipt = Extract<LWorkspaceNoticeReceipt, {
|
|
9
9
|
surface: 'provider';
|
|
@@ -17,11 +17,12 @@ export type ConnectResult<T> = {
|
|
|
17
17
|
code: string;
|
|
18
18
|
message: string;
|
|
19
19
|
requires: string;
|
|
20
|
-
} & Pick<ConnectNeed, 'officialReason'
|
|
20
|
+
} & Partial<Pick<ConnectNeed, 'officialReason' | 'layer' | 'actor' | 'action' | 'lastSeen'>>;
|
|
21
21
|
};
|
|
22
|
+
export type SessionProjectResolution = SessionProjectTarget | HostSessionProjectTarget;
|
|
22
23
|
export interface NativeWorkspaceApi {
|
|
23
24
|
getContractInfo(): ContractInfo;
|
|
24
25
|
ensureReady(provider: LoginProvider): Promise<ConnectResult<ConnectTarget>>;
|
|
25
|
-
resolveSessionProject(sessionId: string, use: WorkspaceUse): Promise<ConnectResult<
|
|
26
|
+
resolveSessionProject(sessionId: string, use: WorkspaceUse, location?: HostWorkspaceResolveContext): Promise<ConnectResult<SessionProjectResolution>>;
|
|
26
27
|
acknowledgeWorkspaceNotice(receipt: WorkspaceNoticeReceipt): Promise<ConnectResult<void>>;
|
|
27
28
|
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { ConnectNeed } from '@ai-workbench/connect/api';
|
|
2
|
+
import { NeedError } from './remote.ts';
|
|
3
|
+
type NeedInput = {
|
|
4
|
+
code: string;
|
|
5
|
+
message: string;
|
|
6
|
+
requires: string;
|
|
7
|
+
} & Partial<Omit<ConnectNeed, 'code' | 'message' | 'requires'>>;
|
|
8
|
+
/** Only the public L Need shape is employee wording; arbitrary errors stay diagnostic. */
|
|
9
|
+
export declare function connectNeedError(value: NeedInput): NeedError;
|
|
10
|
+
export declare function originalConnectNeed(error: unknown): Readonly<ConnectNeed> | undefined;
|
|
11
|
+
export declare function connectNeedMessage(error: unknown): string | undefined;
|
|
12
|
+
export {};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { NeedError } from "./remote.js";
|
|
2
|
+
/** Only the public L Need shape is employee wording; arbitrary errors stay diagnostic. */
|
|
3
|
+
export function connectNeedError(value) {
|
|
4
|
+
const error = new NeedError(value.code, value.message, value.requires);
|
|
5
|
+
if (typeof value.officialReason === 'string')
|
|
6
|
+
error.officialReason = value.officialReason;
|
|
7
|
+
if (typeof value.layer === 'string' && ['enrollment', 'network', 'ssh', 'auth', 'integration', 'settings', 'projects'].includes(value.layer) && typeof value.actor === 'string' && ['employee', 'administrator', 'ai-administrator', 'plugin'].includes(value.actor) && value.action && typeof value.action.id === 'string' && typeof value.action.label === 'string' && typeof value.action.available === 'boolean') {
|
|
8
|
+
error.connectNeed = Object.freeze({ code: value.code, layer: value.layer, message: value.message, requires: value.requires, actor: value.actor, action: Object.freeze({ id: value.action.id, label: value.action.label, available: value.action.available }), ...(typeof value.officialReason === 'string' ? { officialReason: value.officialReason } : {}), ...(typeof value.lastSeen === 'string' ? { lastSeen: value.lastSeen } : {}) });
|
|
9
|
+
}
|
|
10
|
+
return error;
|
|
11
|
+
}
|
|
12
|
+
export function originalConnectNeed(error) {
|
|
13
|
+
const queue = [error], seen = new Set();
|
|
14
|
+
while (queue.length) {
|
|
15
|
+
const item = queue.shift();
|
|
16
|
+
if (!item || typeof item !== 'object' || seen.has(item))
|
|
17
|
+
continue;
|
|
18
|
+
seen.add(item);
|
|
19
|
+
if (item instanceof NeedError && item.connectNeed)
|
|
20
|
+
return item.connectNeed;
|
|
21
|
+
if (item instanceof AggregateError)
|
|
22
|
+
queue.push(...item.errors);
|
|
23
|
+
if (item instanceof Error && item.cause !== undefined)
|
|
24
|
+
queue.push(item.cause);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
export function connectNeedMessage(error) {
|
|
28
|
+
const need = originalConnectNeed(error);
|
|
29
|
+
if (!need)
|
|
30
|
+
return;
|
|
31
|
+
const location = need.action.id === 'select-workspace-location';
|
|
32
|
+
const action = location ? '设置 → 远程主机连接 → 工作区文件来源(' + need.action.label + ')' : need.action.label;
|
|
33
|
+
return need.message + (action ? '\n下一步' + (need.action.available ? '' : '(当前不可用)') + ':' + action + '。' : '');
|
|
34
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { connectNeedError } from "./connect-need.js";
|
|
1
2
|
import { NeedError } from "./remote.js";
|
|
2
3
|
import { isDeepStrictEqual } from 'node:util';
|
|
3
4
|
const fail = (code) => new NeedError(code, '员工电脑的工作区或执行连接已变化,本轮没有继续执行。', '当前工作区绑定与员工端执行器');
|
|
@@ -29,7 +30,7 @@ export async function admitEmployeeLocation(options) {
|
|
|
29
30
|
const result = await api.getLocalWorkspaceContext(request);
|
|
30
31
|
signal?.throwIfAborted();
|
|
31
32
|
if (!result.ok)
|
|
32
|
-
throw
|
|
33
|
+
throw connectNeedError(result.error);
|
|
33
34
|
const c = result.value;
|
|
34
35
|
if (c.version !== 1 || c.source !== 'local' || c.instanceId !== connection.instanceId || c.epoch !== connection.epoch || c.workspaceId !== workspace.id || c.bindingId !== item.bindingId || !absolute(c.directory?.path) || c.directory.path !== workspace.path || !Number.isSafeInteger(c.directory.device) || !Number.isSafeInteger(c.directory.inode))
|
|
35
36
|
throw fail('EMPLOYEE_CONTEXT_CHANGED');
|
|
@@ -6,6 +6,7 @@ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExte
|
|
|
6
6
|
}
|
|
7
7
|
return path;
|
|
8
8
|
};
|
|
9
|
+
import { connectNeedError } from "./connect-need.js";
|
|
9
10
|
import { CODEX_EMPLOYEE_SCOPE_NOTICE } from "./employee-presentation.js";
|
|
10
11
|
import { CloseDiagnostics, withFailureEvidence, closeCode, closeErrorCodes } from "./close-diagnostics.js";
|
|
11
12
|
import { createHash } from 'node:crypto';
|
|
@@ -53,7 +54,7 @@ export async function acceptEmployeeUserTurn(get, employee, agent, api, request)
|
|
|
53
54
|
stable();
|
|
54
55
|
const context = await api.getLocalWorkspaceContext({ instanceId: connection.instanceId, epoch: connection.epoch, workspaceId: workspace.id, expectedBindingId: row.bindingId });
|
|
55
56
|
if (!context.ok)
|
|
56
|
-
throw
|
|
57
|
+
throw connectNeedError(context.error);
|
|
57
58
|
stable();
|
|
58
59
|
const c = context.value, d = { ...c.directory };
|
|
59
60
|
if (c.version !== 1 || c.source !== 'local' || c.instanceId !== connection.instanceId || c.epoch !== connection.epoch || c.workspaceId !== workspace.id || c.bindingId !== row.bindingId || !absolute(d?.path) || d.path !== workspace.path || ![d.device, d.inode].every(n => Number.isSafeInteger(n) && n >= 0))
|
|
@@ -63,7 +64,7 @@ export async function acceptEmployeeUserTurn(get, employee, agent, api, request)
|
|
|
63
64
|
throw fail('LOCAL_USER_TURN_UNCONFIRMED');
|
|
64
65
|
const response = await api.acceptLocalWorkspaceUserTurn({ instanceId: connection.instanceId, epoch: connection.epoch, sessionId: request.sessionId, provider: agent.loginProvider, purpose: 'user-turn', workspaceId: workspace.id, expectedBindingId: row.bindingId, expectedDirectory: { device: d.device, inode: d.inode }, expectedUserTurn: { ...expected } });
|
|
65
66
|
if (!response.ok)
|
|
66
|
-
throw
|
|
67
|
+
throw connectNeedError(response.error);
|
|
67
68
|
stable();
|
|
68
69
|
const a = response.value;
|
|
69
70
|
if (!a || a.version !== 1 || typeof a.acceptanceId !== 'string' || !a.acceptanceId.trim() || a.instanceId !== connection.instanceId || a.epoch !== connection.epoch || a.sessionId !== request.sessionId || a.provider !== agent.loginProvider || a.workspaceId !== workspace.id || a.bindingId !== row.bindingId || a.source !== 'local' || a.directory?.path !== d.path || a.directory?.device !== d.device || a.directory?.inode !== d.inode || a.userTurn?.turn !== expected.turn || a.userTurn?.userMessageId !== expected.userMessageId || ![a.userTurn?.startSequence, a.userTurn?.userMessageSequence].every(n => Number.isSafeInteger(n) && n >= 0) || a.userTurn.userMessageSequence <= a.userTurn.startSequence)
|
|
@@ -86,7 +87,7 @@ export async function validateEmployeeUserTurn(get, employee, api, request, conn
|
|
|
86
87
|
const c = await api.getLocalWorkspaceContext({ instanceId: a.instanceId, epoch: a.epoch, workspaceId: a.workspaceId, expectedBindingId: a.bindingId });
|
|
87
88
|
stable();
|
|
88
89
|
if (!c.ok)
|
|
89
|
-
throw
|
|
90
|
+
throw connectNeedError(c.error);
|
|
90
91
|
if (c.value.version !== 1 || c.value.source !== 'local' || c.value.instanceId !== a.instanceId || c.value.epoch !== a.epoch || c.value.workspaceId !== a.workspaceId || c.value.bindingId !== a.bindingId || c.value.directory.path !== a.directory.path || c.value.directory.device !== a.directory.device || c.value.directory.inode !== a.directory.inode)
|
|
91
92
|
throw fail('EMPLOYEE_CONTEXT_CHANGED');
|
|
92
93
|
const turn = employee.currentUserTurn(request);
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { ConnectApi, ConnectTarget, ConnectionState, RuntimeRequest, Runtime } from './provider.ts';
|
|
2
|
+
import type { NativeWorkspaceApi, HostSessionProjectTarget, LoginProvider } from './connect-contract.ts';
|
|
3
|
+
import type { WorkspaceSourceOptions } from './workspace-source.ts';
|
|
4
|
+
export interface HostWorkspaceGuard {
|
|
5
|
+
readonly signal: AbortSignal;
|
|
6
|
+
readonly revocation: AbortSignal;
|
|
7
|
+
resolve(): Promise<HostSessionProjectTarget>;
|
|
8
|
+
assertCurrent(): void;
|
|
9
|
+
observeRuntime(runtime: Runtime): void;
|
|
10
|
+
dispose(): void;
|
|
11
|
+
}
|
|
12
|
+
/** Current official membership and explicit host selection; never infer source from a pathname. */
|
|
13
|
+
export declare function hostWorkspaceGuard(get: () => ConnectApi | undefined, api: ConnectApi & NativeWorkspaceApi, connection: ConnectionState, target: ConnectTarget, request: RuntimeRequest, workspace: WorkspaceSourceOptions['workspace'] | undefined, provider: LoginProvider): Promise<HostWorkspaceGuard | undefined>;
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { basename } from 'node:path/posix';
|
|
2
|
+
import { awaitL, sameTarget, unwrap } from "./provider.js";
|
|
3
|
+
import { connectNeedError } from "./connect-need.js";
|
|
4
|
+
import { NeedError } from "./remote.js";
|
|
5
|
+
const invalid = () => new NeedError('PROJECT_BINDING_INVALID', '文件来源已改,请新建会话继续;原会话历史仍可查看。', 'L核验的同一主机位置与目录身份');
|
|
6
|
+
const hostIdentity = (v) => JSON.stringify(v ? [v.host, v.username, v.port] : null);
|
|
7
|
+
const absolute = (v) => typeof v === 'string' && v.startsWith('/') && !v.includes('\0');
|
|
8
|
+
function physical(d) {
|
|
9
|
+
if (!d || !absolute(d.requestedPath) || !absolute(d.path) || !(d.parentPath === null || absolute(d.parentPath)) || !d.identity || !d.entryIdentity || ![d.identity.device, d.identity.inode, d.entryIdentity.device, d.entryIdentity.inode].every(x => typeof x === 'string' && x.length > 0) || typeof d.isSymlink !== 'boolean' || !d.permissions || d.permissions.read !== true || d.permissions.execute !== true || !(typeof d.permissions.write === 'boolean' || d.permissions.write === null))
|
|
10
|
+
throw invalid();
|
|
11
|
+
// Directory contents change timestamps; neither timestamp is an execution identity.
|
|
12
|
+
return JSON.stringify([d.requestedPath, d.path, d.parentPath, d.identity.device, d.identity.inode, d.entryIdentity.device, d.entryIdentity.inode, d.isSymlink, d.permissions.read, d.permissions.write, d.permissions.execute]);
|
|
13
|
+
}
|
|
14
|
+
/** Current official membership and explicit host selection; never infer source from a pathname. */
|
|
15
|
+
export async function hostWorkspaceGuard(get, api, connection, target, request, workspace, provider) {
|
|
16
|
+
if (api.getContractInfo().workspaceLocations !== 1 || typeof api.getWorkspaceLocationState !== 'function')
|
|
17
|
+
return;
|
|
18
|
+
const firstState = api.getWorkspaceLocationState();
|
|
19
|
+
if (!workspace) {
|
|
20
|
+
if (firstState.items.some(row => row.source === 'host'))
|
|
21
|
+
throw new NeedError('DEPENDENCY_REQUIRED', '我需要核对当前会话所属工作区,才能使用所选主机文件夹。', '官方会话与唯一workspaceRegistry关系');
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
const registered = { ...await workspace(request) };
|
|
25
|
+
request.signal?.throwIfAborted();
|
|
26
|
+
const state = api.getWorkspaceLocationState(), rows = state.items.filter(row => row.workspaceId === registered.id), selected = rows[0];
|
|
27
|
+
if (rows.length !== 1 || !selected)
|
|
28
|
+
return;
|
|
29
|
+
if (selected.source !== 'host') {
|
|
30
|
+
if (selected.source !== null && !['local', 'legacy-host'].includes(selected.source))
|
|
31
|
+
throw invalid();
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
if (api.getContractInfo().hostWorkspaceResolution !== 1 || typeof api.onWorkspaceLocationState !== 'function' || typeof api.onConnectionState !== 'function')
|
|
35
|
+
throw new NeedError('DEPENDENCY_REQUIRED', '我需要支持主机位置解析的远程主机连接组件;本轮未执行,原话保留。', 'L >=0.7.20 / hostWorkspaceResolution:1');
|
|
36
|
+
if (state.instanceId !== connection.instanceId || state.epoch !== connection.epoch)
|
|
37
|
+
throw invalid();
|
|
38
|
+
if (state.need)
|
|
39
|
+
throw connectNeedError(state.need);
|
|
40
|
+
if (selected.need)
|
|
41
|
+
throw connectNeedError(selected.need);
|
|
42
|
+
if (!registered.id || !absolute(registered.path) || state.phase !== 'ready' || selected.status !== 'selected' || !selected.bindingId)
|
|
43
|
+
throw invalid();
|
|
44
|
+
const trusted = structuredClone(target);
|
|
45
|
+
if (selected.host && (selected.host.host !== trusted.host || selected.host.username !== trusted.username || selected.host.port !== trusted.port))
|
|
46
|
+
throw invalid();
|
|
47
|
+
const captured = structuredClone(selected), stamp = physical(captured.remote), context = { instanceId: connection.instanceId, epoch: connection.epoch, workspaceId: registered.id, expectedBindingId: selected.bindingId };
|
|
48
|
+
const revoked = new AbortController(), offs = [];
|
|
49
|
+
let runtime, disposed = false;
|
|
50
|
+
const current = () => {
|
|
51
|
+
request.signal?.throwIfAborted();
|
|
52
|
+
revoked.signal.throwIfAborted();
|
|
53
|
+
if (disposed || get() !== api || api.getContractInfo().hostWorkspaceResolution !== 1)
|
|
54
|
+
throw invalid();
|
|
55
|
+
const c = api.getConnectionState(), s = api.getWorkspaceLocationState(), items = s.items.filter(row => row.workspaceId === context.workspaceId), item = items[0];
|
|
56
|
+
if (c.phase !== 'connected' || c.instanceId !== context.instanceId || c.epoch !== context.epoch)
|
|
57
|
+
throw new NeedError('CONNECTION_LOST', '远程主机连接已变化,原派发已停止;请新建会话继续。', '本轮原连接代次');
|
|
58
|
+
if (s.instanceId !== context.instanceId || s.epoch !== context.epoch)
|
|
59
|
+
throw invalid();
|
|
60
|
+
if (s.need)
|
|
61
|
+
throw connectNeedError(s.need);
|
|
62
|
+
if (item?.need)
|
|
63
|
+
throw connectNeedError(item.need);
|
|
64
|
+
if (hostIdentity(item?.host) !== hostIdentity(captured.host))
|
|
65
|
+
throw invalid();
|
|
66
|
+
if (s.phase !== 'ready' || items.length !== 1 || item.bindingId !== context.expectedBindingId || item.workspaceId !== context.workspaceId || item.source !== 'host' || item.status !== 'selected' || item.localPath !== captured.localPath || physical(item.remote) !== stamp)
|
|
67
|
+
throw invalid();
|
|
68
|
+
};
|
|
69
|
+
const revoke = (error) => { if (revoked.signal.aborted)
|
|
70
|
+
return; runtime?.closeDiagnostics?.record({ layer: 'adapter', initiator: 'host-workspace.observer', reason: error instanceof NeedError ? error.code : 'HOST_WORKSPACE_REVOKED', action: 'abort', changes: [{ field: 'host-workspace.valid', before: true, after: false }] }); revoked.abort(error); };
|
|
71
|
+
const changed = () => { if (request.signal?.aborted && !revoked.signal.aborted)
|
|
72
|
+
return; try {
|
|
73
|
+
current();
|
|
74
|
+
}
|
|
75
|
+
catch (error) {
|
|
76
|
+
revoke(error);
|
|
77
|
+
} };
|
|
78
|
+
const dispose = () => { if (disposed)
|
|
79
|
+
return; disposed = true; for (const off of offs.splice(0))
|
|
80
|
+
off(); };
|
|
81
|
+
const membership = async () => { current(); const now = await workspace(request); current(); if (now.id !== registered.id || now.path !== registered.path)
|
|
82
|
+
throw invalid(); };
|
|
83
|
+
try {
|
|
84
|
+
current();
|
|
85
|
+
offs.push(api.onWorkspaceLocationState(changed));
|
|
86
|
+
offs.push(api.onConnectionState(changed));
|
|
87
|
+
current();
|
|
88
|
+
return { revocation: revoked.signal, signal: AbortSignal.any([revoked.signal, ...(request.signal ? [request.signal] : [])]), assertCurrent: current, observeRuntime(value) { runtime = value; current(); }, dispose,
|
|
89
|
+
async resolve() {
|
|
90
|
+
try {
|
|
91
|
+
await membership();
|
|
92
|
+
const value = unwrap(await awaitL(() => api.resolveSessionProject(request.sessionId, { provider: provider, purpose: 'user-turn' }, { ...context }), AbortSignal.any([revoked.signal, ...(request.signal ? [request.signal] : [])])));
|
|
93
|
+
await membership();
|
|
94
|
+
if (!value || !('source' in value) || value.source !== 'host' || value.instanceId !== context.instanceId || value.epoch !== context.epoch || value.sessionId !== request.sessionId || value.workspaceId !== context.workspaceId || value.bindingId !== context.expectedBindingId || !Number.isSafeInteger(value.bindingRevision) || value.bindingRevision < 1 || (!value.target || !value.target.session || !sameTarget(trusted, value.target)) || physical(value.directory) !== stamp || value.project?.path !== value.directory.path || value.project?.name !== (basename(value.directory.path) || '/') || 'notice' in value && value.notice !== undefined)
|
|
95
|
+
throw invalid();
|
|
96
|
+
return value;
|
|
97
|
+
}
|
|
98
|
+
catch (error) {
|
|
99
|
+
if (!request.signal?.aborted)
|
|
100
|
+
revoke(error);
|
|
101
|
+
throw error;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
catch (error) {
|
|
107
|
+
dispose();
|
|
108
|
+
throw error;
|
|
109
|
+
}
|
|
110
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { connectNeedMessage } from "./connect-need.js";
|
|
1
2
|
import { CloseDiagnostics, withFailureEvidence, withFirstFailureEvidence, failureEvidenceSummary, closeCode, closeErrorCodes, closeFailure } from "./close-diagnostics.js";
|
|
2
3
|
import { runCatalog } from "./catalog-ssh.js";
|
|
3
4
|
import { createCatalogDiagnostics, CatalogDiagnosticsRemote } from "./catalog-diagnostics.js";
|
|
@@ -258,7 +259,7 @@ export class CodexRemoteAdapter extends LlmAdapter {
|
|
|
258
259
|
if (error instanceof NeedError) {
|
|
259
260
|
const rpc = /RPC 错误码 (-?\d+)/u.exec(error.requires)?.[1];
|
|
260
261
|
const evidence = failureEvidenceSummary(error);
|
|
261
|
-
const message = employeeFailure(error.code, this.agent.loginProvider) + (rpc ? `(技术详情:RPC 错误码 ${rpc})` : '') + (evidence ? ';' + evidence : '');
|
|
262
|
+
const message = (connectNeedMessage(error) ?? employeeFailure(error.code, this.agent.loginProvider)) + (rpc ? `(技术详情:RPC 错误码 ${rpc})` : '') + (evidence ? ';' + evidence : '');
|
|
262
263
|
const login = originalLoginFailure(error);
|
|
263
264
|
let diagnostic = login?.technical ? JSON.stringify(login.technical) : '';
|
|
264
265
|
if (login?.technical && this.reportLoginFailure) {
|
|
@@ -426,6 +427,12 @@ export class CodexRemoteAdapter extends LlmAdapter {
|
|
|
426
427
|
request.signal = modelSignal;
|
|
427
428
|
modelSignal.throwIfAborted();
|
|
428
429
|
}
|
|
430
|
+
if (runtime.contextSignal) {
|
|
431
|
+
modelSignal = AbortSignal.any([modelSignal, runtime.contextSignal]);
|
|
432
|
+
options.signal = modelSignal;
|
|
433
|
+
request.signal = modelSignal;
|
|
434
|
+
modelSignal.throwIfAborted();
|
|
435
|
+
}
|
|
429
436
|
catalog.assertCurrent();
|
|
430
437
|
if (runtime.connection && (runtime.connection.instanceId !== catalog.target.instanceId || runtime.connection.epoch !== catalog.target.epoch) || runtime.identity !== `${catalog.target.target.username}@${catalog.target.target.host}:${catalog.target.target.port}`)
|
|
431
438
|
throw new ModelConfigurationError('模型目录与当前会话可信员工主机不一致');
|
|
@@ -657,6 +664,12 @@ export class CodexRemoteAdapter extends LlmAdapter {
|
|
|
657
664
|
yield { type: 'finish', reason: { kind: stop === 'max_tokens' || stop === 'max_turn_requests' ? 'max-tokens' : 'stop' }, replayState: { response: pointer } };
|
|
658
665
|
}
|
|
659
666
|
catch (error) {
|
|
667
|
+
// ACP cancellation must retain the source revocation that caused it.
|
|
668
|
+
// User cancellation and unrelated signals keep their existing semantics.
|
|
669
|
+
const sourceReason = runtime?.contextSignal?.aborted ? runtime.contextSignal.reason : undefined;
|
|
670
|
+
if (error instanceof NeedError && error.code === 'ABORTED' && sourceReason instanceof NeedError) {
|
|
671
|
+
error = Object.assign(new NeedError(sourceReason.code, sourceReason.message, sourceReason.requires), { ...(sourceReason.connectNeed ? { connectNeed: sourceReason.connectNeed } : {}), ...(sourceReason.officialReason ? { officialReason: sourceReason.officialReason } : {}), cause: new AggregateError([sourceReason, error], 'HOST_SOURCE_REVOKED') });
|
|
672
|
+
}
|
|
660
673
|
turnFailure = error;
|
|
661
674
|
runtime?.closeDiagnostics?.record({ layer: 'adapter', initiator: 'adapter.turn-failure', reason: closeCode(error, signal.aborted ? 'ABORTED' : 'TURN_FAILED'), action: 'failure' });
|
|
662
675
|
recordInformation();
|
package/dist/presentation.js
CHANGED
|
@@ -9,6 +9,7 @@ export function employeeFailure(code, brand = 'codex') {
|
|
|
9
9
|
WORKSPACE_SOURCE_CONFIRMATION_UNAVAILABLE: '我需要打开工作区来源确认,目前无法显示确认动作;本轮未执行,你的原话仍保留。',
|
|
10
10
|
WORKSPACE_SOURCE_SELECTION_UNCONFIRMED: '无法核对工作区来源的确认结果,本轮已停止;你的原话仍保留。',
|
|
11
11
|
WORKSPACE_SOURCE_CONFIRMATION_FAILED: '工作区来源确认未完成,本轮已停止;你的原话仍保留。',
|
|
12
|
+
PROJECT_BINDING_INVALID: '文件来源已改,请新建会话继续;原会话历史仍可查看。',
|
|
12
13
|
WORKSPACE_LOCATION_REQUIRED: '我需要你确认这个工作区使用哪一个文件夹,本轮未执行;你的原话仍保留。',
|
|
13
14
|
WORKSPACE_LOCATION_CONFLICT: '工作区来源选择已更新,本次确认不能覆盖新的选择;本轮未执行,你的原话仍保留。',
|
|
14
15
|
LOCAL_USER_TURN_CHANGED: '本次输入已变化,本轮已停止,请确认对话后重新发送。',
|
package/dist/provider.d.ts
CHANGED
|
@@ -29,6 +29,7 @@ export interface EmployeeExecution {
|
|
|
29
29
|
export interface Runtime {
|
|
30
30
|
identity: string;
|
|
31
31
|
cwd: string;
|
|
32
|
+
contextSignal?: AbortSignal;
|
|
32
33
|
execution?: EmployeeExecution;
|
|
33
34
|
acpTransport?: {
|
|
34
35
|
writable: WritableStream<Uint8Array>;
|
|
@@ -141,7 +142,7 @@ type Result<T> = {
|
|
|
141
142
|
code: string;
|
|
142
143
|
message: string;
|
|
143
144
|
requires: string;
|
|
144
|
-
} & Pick<ConnectNeed, 'officialReason'
|
|
145
|
+
} & Partial<Pick<ConnectNeed, 'officialReason' | 'layer' | 'actor' | 'action' | 'lastSeen'>>;
|
|
145
146
|
};
|
|
146
147
|
/** 消费正式connect-api 0.7公开子集;X不响应L登录票据、不直接调用登录。 */
|
|
147
148
|
export interface ConnectApi extends Pick<LConnectApi, 'getConnectionState' | 'getLoginPromptState' | 'requestLoginPrompt' | 'refreshLoginPrompts'>, Partial<NativeWorkspaceApi>, Partial<Pick<LConnectApi, 'getWorkspaceBindingState' | 'getModelCatalogTarget' | 'onConnectionState' | 'onLoginPromptState' | 'appendRemoteConversationDetail' | 'getRemoteConversationDetailReceipt' | 'listRemoteConversationDetails' | 'getWorkspaceLocationState' | 'getLocalWorkspaceContext' | 'onWorkspaceLocationState' | 'acceptLocalWorkspaceUserTurn' | 'selectWorkspaceLocation'>> {
|
package/dist/provider.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { hostWorkspaceGuard } from "./host-workspace.js";
|
|
2
|
+
import { connectNeedError } from "./connect-need.js";
|
|
1
3
|
import { loginDiagnostic, loginReason } from "./login-diagnostic.js";
|
|
2
4
|
import { NeedError } from "./remote.js";
|
|
3
5
|
import { CONNECTION_REQUIRED, TEST_HOST_NOTICE } from "./presentation.js";
|
|
@@ -38,8 +40,11 @@ function verifiedLogin(api, brand, expected) {
|
|
|
38
40
|
throw new NeedError('CONNECTION_LOST', 'connection binding changed', 'current employee connection');
|
|
39
41
|
requireLoginState(login);
|
|
40
42
|
const reason = loginReason(login, connection, brand);
|
|
41
|
-
if (reason)
|
|
42
|
-
|
|
43
|
+
if (reason) {
|
|
44
|
+
const original = (reason === 'LOGIN_PROVIDER_NEED' || reason === 'LOGIN_PHASE_BLOCKED') && login.instanceId === connection.instanceId ? login.providerNeeds[brand] : undefined;
|
|
45
|
+
const error = original ? connectNeedError(original) : new NeedError(reason === 'LOGIN_REQUIRED' ? 'LOGIN_REQUIRED' : 'AUTH_STATUS_INVALID', reason === 'LOGIN_REQUIRED' ? '需要登录,请到设置 → 远程主机连接 点登录' : '无法确认当前品牌的登录状态', 'L核验的本品牌登录状态');
|
|
46
|
+
throw Object.assign(error, { technical: loginDiagnostic('verified-login', reason, brand, connection, login), ...(typeof login.providerNeeds[brand]?.officialReason === 'string' ? { officialReason: login.providerNeeds[brand].officialReason } : {}) });
|
|
47
|
+
}
|
|
43
48
|
return { ...connection };
|
|
44
49
|
}
|
|
45
50
|
/** Wait only for L's explicit in-progress evidence. No timer, polling, refresh or authorization. */
|
|
@@ -102,12 +107,12 @@ async function waitForLogin(api, brand, expected, get, signal) {
|
|
|
102
107
|
async function ensureReady(api, brand, signal) {
|
|
103
108
|
const result = await awaitL(() => api.ensureReady(brand), signal);
|
|
104
109
|
if (!result.ok)
|
|
105
|
-
throw Object.assign(
|
|
110
|
+
throw Object.assign(connectNeedError(result.error), { technical: loginDiagnostic('ensure-ready', 'L_ENSURE_READY_FAILED', brand, api.getConnectionState(), api.getLoginPromptState(), result.error.officialReason), ...(typeof result.error.officialReason === 'string' ? { officialReason: result.error.officialReason } : {}) });
|
|
106
111
|
return result.value;
|
|
107
112
|
}
|
|
108
113
|
export function unwrap(result) {
|
|
109
114
|
if (!result.ok)
|
|
110
|
-
throw Object.assign(
|
|
115
|
+
throw Object.assign(connectNeedError(result.error), typeof result.error.officialReason === 'string' ? { officialReason: result.error.officialReason } : {});
|
|
111
116
|
return result.value;
|
|
112
117
|
}
|
|
113
118
|
export function validateTarget(t) {
|
|
@@ -155,6 +160,30 @@ export function sameTarget(a, b) {
|
|
|
155
160
|
}
|
|
156
161
|
export function connectProvider(get, options = {}) {
|
|
157
162
|
const agent = options.agent ?? CODEX_AGENT;
|
|
163
|
+
const hostScopes = new WeakMap();
|
|
164
|
+
const hostMethods = new WeakMap();
|
|
165
|
+
const retainHostScope = (runtime, scope) => {
|
|
166
|
+
hostScopes.get(runtime)?.dispose();
|
|
167
|
+
hostScopes.set(runtime, scope);
|
|
168
|
+
runtime.contextSignal = scope.revocation;
|
|
169
|
+
if (hostMethods.has(runtime))
|
|
170
|
+
return;
|
|
171
|
+
const original = { dispose: runtime.dispose, detach: runtime.detach };
|
|
172
|
+
hostMethods.set(runtime, original);
|
|
173
|
+
const release = () => { hostScopes.get(runtime)?.dispose(); hostScopes.delete(runtime); };
|
|
174
|
+
runtime.dispose = async (cause) => { try {
|
|
175
|
+
await original.dispose?.(cause);
|
|
176
|
+
}
|
|
177
|
+
finally {
|
|
178
|
+
release();
|
|
179
|
+
} };
|
|
180
|
+
runtime.detach = async (cause) => { try {
|
|
181
|
+
await original.detach?.(cause);
|
|
182
|
+
}
|
|
183
|
+
finally {
|
|
184
|
+
release();
|
|
185
|
+
} };
|
|
186
|
+
};
|
|
158
187
|
// Keep the legacy source SDK wrapper's strip-only import graph free of broker runtime syntax.
|
|
159
188
|
// Cache the initialization promise so concurrent sessions share this provider's binding lifetimes.
|
|
160
189
|
let employee;
|
|
@@ -203,6 +232,9 @@ export function connectProvider(get, options = {}) {
|
|
|
203
232
|
request.localAdmissionCheck?.();
|
|
204
233
|
admissions.set(request, { api, target, connection });
|
|
205
234
|
sourceStage = 'local-admission';
|
|
235
|
+
const locations = api.getContractInfo().workspaceLocations === 1 ? api.getWorkspaceLocationState?.() : undefined;
|
|
236
|
+
if (locations && (locations.phase === 'failed' || locations.phase === 'disposed') && locations.instanceId === connection.instanceId && locations.epoch === connection.epoch && locations.need)
|
|
237
|
+
throw connectNeedError(locations.need);
|
|
206
238
|
await options.local?.admit?.(request, api, connection, target);
|
|
207
239
|
confirmedSource?.assertCurrent();
|
|
208
240
|
}
|
|
@@ -228,88 +260,109 @@ export function connectProvider(get, options = {}) {
|
|
|
228
260
|
request.localAdmissionCheck?.();
|
|
229
261
|
if (!request.sessionId)
|
|
230
262
|
throw new NeedError('PROJECT_BINDING_REQUIRED', '我需要当前会话和已选择的工作区,请使用原生工作区入口', 'DSH会话与原生工作区');
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
local
|
|
236
|
-
|
|
263
|
+
const host = await hostWorkspaceGuard(get, api, connection, target, request, options.workspaceSource?.workspace, agent.loginProvider);
|
|
264
|
+
try {
|
|
265
|
+
if (!host && options.employee) {
|
|
266
|
+
employee ??= import("./employee-provider.js").then(module => module.createEmployeeProvider(get, options, agent));
|
|
267
|
+
const local = await (await employee)({ api, target, connection, request, stable: signal => request.timing.run('connection-confirm', () => waitForLogin(api, agent.loginProvider, connection, get, signal)) });
|
|
268
|
+
if (local) {
|
|
269
|
+
local.loginRequired = () => offerLogin(api, request, get, agent);
|
|
270
|
+
return local;
|
|
271
|
+
}
|
|
237
272
|
}
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
273
|
+
if (!host && agent.loginProvider === 'claude' && options.local) {
|
|
274
|
+
const runtime = await options.local(request, api, connection, target);
|
|
275
|
+
if (runtime) {
|
|
276
|
+
try {
|
|
277
|
+
await stable();
|
|
278
|
+
}
|
|
279
|
+
catch (error) {
|
|
280
|
+
await runtime.dispose?.();
|
|
281
|
+
throw error;
|
|
282
|
+
}
|
|
283
|
+
return runtime;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
if (!host) {
|
|
287
|
+
await options.notice?.reconcile(request, api);
|
|
288
|
+
await stable();
|
|
289
|
+
}
|
|
290
|
+
const resolved = await request.timing.run('workspace-resolve', async () => host ? host.resolve() : unwrap(await awaitL(() => api.resolveSessionProject(request.sessionId, { provider: agent.loginProvider, purpose: 'user-turn' }), request.signal)));
|
|
291
|
+
await stable();
|
|
292
|
+
host?.assertCurrent();
|
|
293
|
+
if (!resolved || resolved.instanceId !== connection.instanceId || resolved.epoch !== connection.epoch)
|
|
294
|
+
throw new NeedError('CONNECTION_LOST', '工作区解析回复已失效', '当前连接代次');
|
|
295
|
+
validateTarget(resolved.target);
|
|
296
|
+
const project = resolved.project;
|
|
297
|
+
const source = resolved.source;
|
|
298
|
+
if (resolved.sessionId !== request.sessionId || !resolved.workspaceId || !resolved.bindingId || !Number.isSafeInteger(resolved.bindingRevision) || resolved.bindingRevision < 1 || !sameTarget(target, resolved.target) || !project || (!host && (source !== undefined && source !== 'legacy-host' || typeof project.name !== 'string' || !project.name || project.name === '.' || project.name === '..' || /[\\/\x00-\x1f\x7f]/.test(project.name) || project.path !== resolved.target.projectRoot + '/' + project.name)))
|
|
299
|
+
throw new NeedError('PROJECT_BINDING_INVALID', '工作区绑定与当前会话或可信主机不一致', 'L核验的原生工作区绑定');
|
|
300
|
+
if (request.cwd && request.cwd !== project.path || request.bindingId && request.bindingId !== resolved.bindingId || request.workspaceId && request.workspaceId !== resolved.workspaceId)
|
|
301
|
+
throw new NeedError(host ? 'PROJECT_BINDING_INVALID' : 'SESSION_CONTEXT_MISMATCH', host ? '文件来源已改,请新建会话继续;原会话历史仍可查看。' : '原会话的工作区绑定已改变,请新建会话', '原工作区绑定');
|
|
302
|
+
const details = await request.timing.run('details-open', async () => options.details?.(request, api, connection, target));
|
|
303
|
+
try {
|
|
304
|
+
if (details)
|
|
305
|
+
request.timing.bind(detail => details.record(detail));
|
|
306
|
+
await stable();
|
|
307
|
+
host?.assertCurrent();
|
|
308
|
+
let workspaceNotice = undefined;
|
|
309
|
+
const notice = !host && 'notice' in resolved ? resolved.notice : undefined;
|
|
310
|
+
if (notice) {
|
|
311
|
+
if (!options.notice)
|
|
312
|
+
throw new NeedError('DEPENDENCY_REQUIRED', '我需要官方会话持久记录来确认工作区说明', '官方会话与持久化服务');
|
|
313
|
+
workspaceNotice = await request.timing.run('workspace-notice', () => options.notice.deliver(notice, resolved, request, api));
|
|
314
|
+
await stable();
|
|
315
|
+
}
|
|
316
|
+
if (!options.open)
|
|
317
|
+
throw new NeedError('RUNTIME_REQUIRED', '我需要主机运行适配器', '主机broker适配');
|
|
318
|
+
const observedBinding = host ? undefined : api.getWorkspaceBindingState?.();
|
|
319
|
+
const validateContext = async (ignoreCancellation = false) => {
|
|
320
|
+
const check = () => waitForLogin(api, agent.loginProvider, connection, get, ignoreCancellation ? undefined : request.signal);
|
|
321
|
+
await check();
|
|
322
|
+
if (host && !ignoreCancellation)
|
|
323
|
+
await host.resolve();
|
|
324
|
+
if (observedBinding) {
|
|
325
|
+
const current = api.getWorkspaceBindingState();
|
|
326
|
+
if (current.instanceId !== observedBinding.instanceId || !current.items.some(item => item.bindingId === resolved.bindingId && item.workspaceId === resolved.workspaceId && item.status === 'ready'))
|
|
327
|
+
throw new NeedError('SESSION_CONTEXT_MISMATCH', '当前工作区绑定已改变,原会话操作未执行', '原生会话原绑定');
|
|
328
|
+
}
|
|
329
|
+
await check();
|
|
330
|
+
if (!ignoreCancellation)
|
|
331
|
+
host?.assertCurrent();
|
|
332
|
+
};
|
|
333
|
+
host?.assertCurrent();
|
|
334
|
+
const runtime = await options.open(resolved.target, project, { ...request, ...(host ? { signal: host.signal } : {}), agent, bindingId: resolved.bindingId, workspaceId: resolved.workspaceId, bindingRevision: resolved.bindingRevision, detailChannel: details, validateContext });
|
|
242
335
|
try {
|
|
243
336
|
await stable();
|
|
337
|
+
host?.observeRuntime(runtime);
|
|
244
338
|
}
|
|
245
339
|
catch (error) {
|
|
246
340
|
await runtime.dispose?.();
|
|
247
341
|
throw error;
|
|
248
342
|
}
|
|
343
|
+
if (host)
|
|
344
|
+
retainHostScope(runtime, host);
|
|
345
|
+
// The transport can outlive a turn; presentation belongs only to this invocation.
|
|
346
|
+
runtime.workspaceNotice = workspaceNotice || undefined;
|
|
347
|
+
runtime.details = details ?? runtime.details;
|
|
348
|
+
runtime.binding = { bindingId: resolved.bindingId, workspaceId: resolved.workspaceId, revision: resolved.bindingRevision };
|
|
349
|
+
runtime.connection = { ...connection };
|
|
350
|
+
runtime.loginRequired = () => offerLogin(api, request, get, agent);
|
|
249
351
|
return runtime;
|
|
250
352
|
}
|
|
251
|
-
}
|
|
252
|
-
await options.notice?.reconcile(request, api);
|
|
253
|
-
await stable();
|
|
254
|
-
const resolved = await request.timing.run('workspace-resolve', async () => unwrap(await awaitL(() => api.resolveSessionProject(request.sessionId, { provider: agent.loginProvider, purpose: 'user-turn' }), request.signal)));
|
|
255
|
-
await stable();
|
|
256
|
-
if (!resolved || resolved.instanceId !== connection.instanceId || resolved.epoch !== connection.epoch)
|
|
257
|
-
throw new NeedError('CONNECTION_LOST', '工作区解析回复已失效', '当前连接代次');
|
|
258
|
-
validateTarget(resolved.target);
|
|
259
|
-
const project = resolved.project;
|
|
260
|
-
if (resolved.sessionId !== request.sessionId || !resolved.workspaceId || !resolved.bindingId || !Number.isSafeInteger(resolved.bindingRevision) || resolved.bindingRevision < 1 || !sameTarget(target, resolved.target) || !project || typeof project.name !== 'string' || !project.name || project.name === '.' || project.name === '..' || /[\\/\x00-\x1f\x7f]/.test(project.name) || project.path !== resolved.target.projectRoot + '/' + project.name)
|
|
261
|
-
throw new NeedError('PROJECT_BINDING_INVALID', '工作区绑定与当前会话或可信主机不一致', 'L核验的原生工作区绑定');
|
|
262
|
-
if (request.cwd && request.cwd !== project.path || request.bindingId && request.bindingId !== resolved.bindingId || request.workspaceId && request.workspaceId !== resolved.workspaceId)
|
|
263
|
-
throw new NeedError('SESSION_CONTEXT_MISMATCH', '原会话的工作区绑定已改变,请新建会话', '原工作区绑定');
|
|
264
|
-
const details = await request.timing.run('details-open', async () => options.details?.(request, api, connection, target));
|
|
265
|
-
try {
|
|
266
|
-
if (details)
|
|
267
|
-
request.timing.bind(detail => details.record(detail));
|
|
268
|
-
await stable();
|
|
269
|
-
let workspaceNotice = undefined;
|
|
270
|
-
if (resolved.notice) {
|
|
271
|
-
if (!options.notice)
|
|
272
|
-
throw new NeedError('DEPENDENCY_REQUIRED', '我需要官方会话持久记录来确认工作区说明', '官方会话与持久化服务');
|
|
273
|
-
workspaceNotice = await request.timing.run('workspace-notice', () => options.notice.deliver(resolved.notice, resolved, request, api));
|
|
274
|
-
await stable();
|
|
275
|
-
}
|
|
276
|
-
if (!options.open)
|
|
277
|
-
throw new NeedError('RUNTIME_REQUIRED', '我需要主机运行适配器', '主机broker适配');
|
|
278
|
-
const observedBinding = api.getWorkspaceBindingState?.();
|
|
279
|
-
const validateContext = async (ignoreCancellation = false) => {
|
|
280
|
-
const check = () => waitForLogin(api, agent.loginProvider, connection, get, ignoreCancellation ? undefined : request.signal);
|
|
281
|
-
await check();
|
|
282
|
-
if (observedBinding) {
|
|
283
|
-
const current = api.getWorkspaceBindingState();
|
|
284
|
-
if (current.instanceId !== observedBinding.instanceId || !current.items.some(item => item.bindingId === resolved.bindingId && item.workspaceId === resolved.workspaceId && item.status === 'ready'))
|
|
285
|
-
throw new NeedError('SESSION_CONTEXT_MISMATCH', '当前工作区绑定已改变,原会话操作未执行', '原生会话原绑定');
|
|
286
|
-
}
|
|
287
|
-
await check();
|
|
288
|
-
};
|
|
289
|
-
const runtime = await options.open(resolved.target, project, { ...request, agent, bindingId: resolved.bindingId, workspaceId: resolved.workspaceId, bindingRevision: resolved.bindingRevision, detailChannel: details, validateContext });
|
|
290
|
-
try {
|
|
291
|
-
await stable();
|
|
292
|
-
}
|
|
293
353
|
catch (error) {
|
|
294
|
-
|
|
354
|
+
try {
|
|
355
|
+
await details?.flush();
|
|
356
|
+
}
|
|
357
|
+
catch (diagnostic) {
|
|
358
|
+
const code = (value) => value instanceof NeedError && /^[A-Z][A-Z0-9_]*$/.test(value.code) ? value.code : 'UNKNOWN_FAILURE';
|
|
359
|
+
throw new NeedError('CONVERSATION_DETAILS_FAILED', '技术详情未能保存;本轮已停止', `原错误代码 ${code(error)};诊断错误代码 ${code(diagnostic)}`);
|
|
360
|
+
}
|
|
295
361
|
throw error;
|
|
296
362
|
}
|
|
297
|
-
// The transport can outlive a turn; presentation belongs only to this invocation.
|
|
298
|
-
runtime.workspaceNotice = workspaceNotice || undefined;
|
|
299
|
-
runtime.details = details ?? runtime.details;
|
|
300
|
-
runtime.binding = { bindingId: resolved.bindingId, workspaceId: resolved.workspaceId, revision: resolved.bindingRevision };
|
|
301
|
-
runtime.connection = { ...connection };
|
|
302
|
-
runtime.loginRequired = () => offerLogin(api, request, get, agent);
|
|
303
|
-
return runtime;
|
|
304
363
|
}
|
|
305
364
|
catch (error) {
|
|
306
|
-
|
|
307
|
-
await details?.flush();
|
|
308
|
-
}
|
|
309
|
-
catch (diagnostic) {
|
|
310
|
-
const code = (value) => value instanceof NeedError && /^[A-Z][A-Z0-9_]*$/.test(value.code) ? value.code : 'UNKNOWN_FAILURE';
|
|
311
|
-
throw new NeedError('CONVERSATION_DETAILS_FAILED', '技术详情未能保存;本轮已停止', `原错误代码 ${code(error)};诊断错误代码 ${code(diagnostic)}`);
|
|
312
|
-
}
|
|
365
|
+
host?.dispose();
|
|
313
366
|
throw error;
|
|
314
367
|
}
|
|
315
368
|
};
|
package/dist/remote.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { ConnectNeed } from '@ai-workbench/connect/api';
|
|
1
2
|
import type { LoginDiagnostic } from './login-diagnostic.ts';
|
|
2
3
|
import type { ChildProcessWithoutNullStreams } from 'node:child_process';
|
|
3
4
|
/** Development-only direct SSH. Product transports must consume L's exported sshArgs (v0.2). */
|
|
@@ -19,6 +20,7 @@ export declare class NeedError extends Error {
|
|
|
19
20
|
requires: string;
|
|
20
21
|
technical?: LoginDiagnostic;
|
|
21
22
|
officialReason?: string;
|
|
23
|
+
connectNeed?: Readonly<ConnectNeed>;
|
|
22
24
|
constructor(code: string, message: string, requires: string);
|
|
23
25
|
}
|
|
24
26
|
export declare function shellQuote(value: string): string;
|
package/dist/remote.js
CHANGED
package/dist/workspace-source.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { withFailureEvidence } from "./failure-evidence.js";
|
|
2
|
+
import { connectNeedError } from "./connect-need.js";
|
|
2
3
|
import { isDeepStrictEqual } from 'node:util';
|
|
3
4
|
import { sanitizeDisplayLabel } from "./conversation-details.js";
|
|
4
5
|
import { NeedError } from "./remote.js";
|
|
@@ -82,7 +83,7 @@ export async function confirmLegacyWorkspaceSource(get, api, request, hooks) {
|
|
|
82
83
|
const selected = await api.selectWorkspaceLocation({ instanceId: connection.instanceId, epoch: connection.epoch, workspaceId: workspace.id, expectedBindingId: frozen.bindingId, source: 'local' });
|
|
83
84
|
request.signal?.throwIfAborted();
|
|
84
85
|
if (!selected.ok)
|
|
85
|
-
throw
|
|
86
|
+
throw connectNeedError(selected.error);
|
|
86
87
|
const current = await stable(false), value = selected.value, returned = value.items.filter(item => item.workspaceId === workspace.id);
|
|
87
88
|
if (value.phase !== 'ready' || value.instanceId !== connection.instanceId || value.epoch !== connection.epoch || returned.length !== 1 || returned[0].source !== 'local' || returned[0].status !== 'selected' || !returned[0].bindingId || returned[0].bindingId === frozen.bindingId || returned[0].localPath !== workspace.path || current.source !== 'local' || current.status !== 'selected' || current.bindingId !== returned[0].bindingId)
|
|
88
89
|
throw fail('WORKSPACE_SOURCE_SELECTION_UNCONFIRMED');
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ai-workbench/remote-agent",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.17",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
"zod": "4.6.7"
|
|
40
40
|
},
|
|
41
41
|
"devDependencies": {
|
|
42
|
-
"@ai-workbench/connect": "file:test/fixtures/ai-workbench-connect-0.7.
|
|
42
|
+
"@ai-workbench/connect": "file:test/fixtures/ai-workbench-connect-0.7.20.tgz",
|
|
43
43
|
"@ai-workbench/network": "file:test/fixtures/ai-workbench-network-0.4.4.tgz",
|
|
44
44
|
"@deepseek-ai/cordis": "4.0.4",
|
|
45
45
|
"@deepseek-ai/cordis-plugin-group": "1.0.4",
|
|
@@ -68,7 +68,7 @@
|
|
|
68
68
|
"zustand": "4.4.7"
|
|
69
69
|
},
|
|
70
70
|
"peerDependencies": {
|
|
71
|
-
"@ai-workbench/connect": "~0.7.
|
|
71
|
+
"@ai-workbench/connect": "~0.7.20",
|
|
72
72
|
"@ai-workbench/network": "~0.4.4",
|
|
73
73
|
"@deepseek-ai/cordis": "4.0.4",
|
|
74
74
|
"@deepseek-ai/dsh-agent": "0.2.0-rc.2",
|