@ai-workbench/remote-agent 0.0.0-stage → 0.8.0

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 CHANGED
@@ -1,3 +1,54 @@
1
- # Temporary Holding Version
1
+ 在 DSH「插件 → 添加插件」填 `@ai-workbench/remote-agent@0.8.0`(由编排发布后可用),或运行 `dsh plugin --profile desktop add @ai-workbench/remote-agent@0.8.0`。
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ # 远程ACP插件
4
+
5
+ 一个插件提供 **Claude(远程主机)** 和 **Codex(远程主机)**,通过员工主机上的官方CLI运行。Codex保留单次模式;Claude固定会话模式。只用官方模型列表、插件管理和配置入口,不提供自制面板、问答或登录按钮。登录和原生工作区绑定唯一归连接插件L。
6
+
7
+ 本轮为公开npm发布准备版0.8.0,包名采用`@ai-workbench/remote-agent`以涵盖两个品牌;发布前自测从固定tgz绝对路径安装。X仅准备,不执行实际发布,编排统一发布后才可按包名安装。正式L0.7.0已交付;开发fixture逐字节保留其固定包,公开类型与SSH导出从该包消费。运行时仍要求L的`getContractInfo()`同时返回API0.7.0、implemented、native-local-v1;旧L包、draft.3或只有版本号的候选会明确报缺项。依赖范围为`~0.7.0`,L由官方插件管理单独安装,不打进X。组件验证不代表真实开通、模型或leo E通过;HanaMesh另验。
8
+
9
+ ## 员工路径
10
+
11
+ 从官方插件管理安装,照常使用默认工作区/会话/模型入口。原生添加或选择本机工作区后,首次实际使用远程模型由L建立/绑定同名远端工作区。本地文件不自动上传,X不猜目录、不选首项。首次说明使用官方普通回复;先经官方会话提交、持久化并读回核对,才向L确认。持久记录不等于员工已看见,GUI另验;日志丢失后的未知claim由L保留,不宣称端到端恰好一次。
12
+
13
+ 未登录时两个模型均回复:**需要登录,请到设置 → 远程主机连接 点登录**。无L或缺正式能力时直接说明缺项,不让员工填写主机、用户、路径或密钥。
14
+
15
+ ## 首版审批与能力边界
16
+
17
+ 只使用官方DSH审批:唯一的`allow_once`对应允许一次,唯一的`reject_once`对应拒绝。当前固定官方版本没有“本会话总是允许”结果;以后有公开能力才映射,不伪造。
18
+
19
+ **首版不支持:`allow_always`、`reject_always`、同一种类有多个不同optionId的歧义选项。** 这些选项不出现在官方审批中;当前请求遇到时会在普通回复明确说明具体名称、标识与种类。无法映射的结果不授权,不取第一项、不静默降为允许一次、不使用userQuestions。取消或断线会撤销旧审批,迟到应答无效;只有真实本轮完成才结算其应答状态。
20
+
21
+ 文本、思考、工具步骤/正文/补丁、计划、通知映射为官方普通流;远端工具不变成本机工具调用。图片输入由官方附件服务读取已核验字节,经ACP传给主机,不传员工文件路径。非文本输出无法得到官方持久附件引用时,明确说明收到的内容类型与所缺能力,不假造图片引用。远端结构化提问、未映射扩展会明确提示并取消,不静默丢弃。
22
+
23
+ 首次系统/开发者上下文按协商的ACP系统能力传入。**首版不支持:已有Claude会话中立即生效的系统/开发者上下文变更。** 固定适配器说明恢复后的变更要到下次上下文整理才生效;X发现变更会停止本轮派发并提示新建会话,避免按旧约束执行,不自动整理、重试或新建替代会话。未声明系统能力的agent会明确报缺项。
24
+
25
+ `usage_update`的used/size是上下文占用,不能当作本轮输入/输出计费量;远端真实prompt响应usage及Claude quota令牌计数是会话累计,按保存在当前实例/恢复标识中的可信基线减差后进入DSH本轮用量;重复值不重复结算。恢复缺少基线、计数缺失、回退或来源改变时明确不计入该轮,重建可信基线,不能把历史用量算成本轮。
26
+
27
+ 会话模式跨轮保留同一ACP客户端和主机进程;关闭实际agent/session或插件时精确清理。重开恢复优先协商resume,无能力时明确使用load,不静默新建。恢复标识绑定品牌、员工目标、DSH会话和L工作区;复制/分叉不能沿用父远端会话。客户端丢失留下的孤儿由管理员确认处理,不自动结束或重开。无TTL或隐式淘汰上限。
28
+
29
+ Codex单次模式首版仅文本,不转接中途审批;图片或系统/开发者上下文需使用会话模式。文件传输、远端VFS与自动同步不在本轮。其他模型不锁定、不拦截、不记录。
30
+
31
+ ## 管理员配置
32
+
33
+ 在官方设置中使用“打开配置文件”,修改**已有同id插件条目**的config。保留官方settingsNs,不注册额外设置节;原生提供方编辑卡是否可编辑以官方实际能力为准。不要新增重复insert条目,不修改官方默认服务或布局。
34
+
35
+ | 设置 | 默认 | 范围与用途 |
36
+ |---|---|---|
37
+ | mode | session | Codex可选session/one-shot,切换后新建会话;Claude固定session |
38
+ | acpMaxMessageBytes | 4194304 | 正整数,会话单条消息容量,默认4MiB;超限不截断 |
39
+ | brokerMaxFrameBytes | 2097152 | 正整数,主机回传单帧容量,默认2MiB |
40
+ | brokerControlMaxBytes | 1048576 | 正整数,主机会话管理回复容量,默认1MiB |
41
+ | oneShotSandbox | workspace-write | Codex单次可选read-only/workspace-write/danger-full-access |
42
+ | oneShotApproval | never | 当前单次通道无中途交互审批;不代表自动批准 |
43
+
44
+ 六项沿用owner已定默认,无新增设置或策略阈值;非法值拒绝,删除覆盖遵循官方配置继承。SSH可执行程序从当前PATH发现,不写死某一系统路径。原始错误码保留在官方失败代码/技术详情,原始stderr、凭据和URL不展开。
45
+
46
+ 仅新增中文文案和验证;保留已有英文名称及既有locale机制。提供方名称不附状态后缀,由原注册replace更新,不自造刷新或客户端品牌分支。
47
+
48
+ ## 安装产物
49
+
50
+ 包内已含`dist`中的JavaScript、类型和客户端产物及locale/官方patch,无TypeScript源码、测试、证据或主机诊断脚本。无需postinstall、prepare或下载/编译步骤;依赖使用已固定的公开npm版本并由官方安装器正常解析,不把第三方源码/测试夹入X包。`@deepseek-ai/*`均为已测精确peer,由客户端提供;连接插件L另装。原配置id/settingsNs与两个provider id保持,旧包升级需经官方管理先卸载再装新包,不并装两份X。
51
+
52
+ ## 验证范围
53
+
54
+ 当前准备结果与限制记入源码仓`docs/evidence/2026-10-21-X-npm-prep/`(不进npm包)。先前真实emp-a协议诊断及固定三件套GUI是不同轮次,不能代替本次新包安装门。L完整绑定/登录、真实模型、物理断网、HanaMesh及leo E以实际已执行证据为准。主机命令安装来自S,X不修改S仓/安装脚本或登录状态。
@@ -0,0 +1,3 @@
1
+ - insert:
2
+ - id: ai-workbench-codex
3
+ name: '@ai-workbench/remote-agent'
package/dist/acp.d.ts ADDED
@@ -0,0 +1,46 @@
1
+ import * as stable from '@agentclientprotocol/sdk';
2
+ import * as acp from '@agentclientprotocol/sdk/unstable';
3
+ import { NeedError, type Need } from './remote.ts';
4
+ import type { BrokerTransport } from './broker.ts';
5
+ export type ApprovalOutcome = 'allowed-once' | 'rejected' | 'cancelled' | 'unavailable';
6
+ export interface AcpHooks {
7
+ onUpdate?: (notification: acp.SessionNotification) => void;
8
+ onResponse?: (response: acp.PromptResponse) => void;
9
+ approve?: (request: acp.RequestPermissionRequest, signal?: AbortSignal) => Promise<ApprovalOutcome>;
10
+ onNeed?: (need: Need) => void;
11
+ broker?: BrokerTransport;
12
+ }
13
+ export interface AcpStartContext {
14
+ systemPrompt?: string;
15
+ }
16
+ export type AcpStartMethod = 'session/new' | 'session/resume' | 'session/load';
17
+ /** ACP v1 with explicitly negotiated SDK 2.0 lifecycle/context extensions. */
18
+ export declare class AcpSession {
19
+ private connection;
20
+ private hooks;
21
+ private sessionId?;
22
+ private turnSignal?;
23
+ private turnToken?;
24
+ private busy;
25
+ private startAttempted;
26
+ private negotiated;
27
+ private method?;
28
+ private ready;
29
+ private updating;
30
+ private cwd?;
31
+ private systemPrompt;
32
+ constructor(transport: acp.Stream | acp.AgentApp | stable.AgentApp, hooks: AcpHooks);
33
+ private observe;
34
+ get id(): string | undefined;
35
+ get capabilities(): Readonly<acp.AgentCapabilities>;
36
+ get startMethod(): AcpStartMethod | undefined;
37
+ validatePrompt(prompt: acp.ContentBlock[]): void;
38
+ validateContext(context: AcpStartContext): void;
39
+ setHooks(hooks: AcpHooks): void;
40
+ get closed(): Promise<void>;
41
+ start(cwd: string, resumeId?: string, signal?: AbortSignal, context?: AcpStartContext): Promise<string>;
42
+ updateContext(context: AcpStartContext, signal?: AbortSignal): Promise<AcpStartMethod | undefined>;
43
+ prompt(input: string | acp.ContentBlock[], signal?: AbortSignal): Promise<acp.StopReason>;
44
+ close(): void;
45
+ }
46
+ export declare function acpNeed(error: unknown): NeedError;
package/dist/acp.js ADDED
@@ -0,0 +1,303 @@
1
+ import * as acp from '@agentclientprotocol/sdk/unstable';
2
+ import { NeedError } from "./remote.js";
3
+ /** ACP v1 with explicitly negotiated SDK 2.0 lifecycle/context extensions. */
4
+ export class AcpSession {
5
+ connection;
6
+ hooks;
7
+ sessionId;
8
+ turnSignal;
9
+ turnToken;
10
+ busy = false;
11
+ startAttempted = false;
12
+ negotiated = {};
13
+ method;
14
+ ready = false;
15
+ updating = false;
16
+ cwd;
17
+ systemPrompt = '';
18
+ constructor(transport, hooks) {
19
+ this.hooks = hooks;
20
+ const app = acp.client({ name: '@ai-workbench/remote-agent' })
21
+ .onNotification(acp.methods.client.session.update, ({ params: notification }) => {
22
+ if (this.busy && notification.sessionId === this.sessionId) {
23
+ // Session metadata is not evidence that the current prompt was accepted.
24
+ if (['agent_message_chunk', 'agent_thought_chunk', 'tool_call', 'tool_call_update'].includes(notification.update.sessionUpdate))
25
+ this.hooks.broker?.acknowledge(acp.methods.agent.session.prompt);
26
+ const meta = record(notification._meta);
27
+ if (meta && Object.keys(meta).length)
28
+ this.hooks.onNeed?.({ code: 'ACP_EXTENSION_UNSUPPORTED', message: `首版不支持:远端通知扩展字段 ${Object.keys(meta).map(safeLabel).join('、')};扩展值未展开`, requires: '对应的公开扩展映射' });
29
+ this.hooks.onUpdate?.(notification);
30
+ }
31
+ })
32
+ .onRequest(acp.methods.client.session.requestPermission, async ({ params: request, signal: rpcSignal, requestId }) => {
33
+ if (request.sessionId !== this.sessionId || !this.busy || this.turnSignal?.aborted)
34
+ return { outcome: { outcome: 'cancelled' } };
35
+ this.hooks.broker?.acknowledge(acp.methods.agent.session.prompt);
36
+ const toolName = request.toolCall.name ?? record(request.toolCall._meta?.claudeCode)?.toolName;
37
+ if (toolName === 'AskUserQuestion') {
38
+ this.hooks.onNeed?.({ code: 'INTERACTION_UNSUPPORTED', message: '首版不支持:远端提问;已取消,未提交答案', requires: '可承载此提问的官方交互接口' });
39
+ return { outcome: { outcome: 'cancelled' } };
40
+ }
41
+ const eligible = request.options.filter(option => {
42
+ const single = option.kind === 'allow_once' || option.kind === 'reject_once';
43
+ const sameKind = request.options.filter(candidate => candidate.kind === option.kind);
44
+ const unique = single && sameKind.length === 1 && request.options.filter(candidate => candidate.optionId === option.optionId).length === 1;
45
+ if (!unique)
46
+ this.hooks.onNeed?.({ code: 'APPROVAL_OPTION_UNSUPPORTED', message: `首版不支持:${option.name} / ${option.optionId} / ${option.kind}`, requires: '唯一对应的允许一次或拒绝一次选项' });
47
+ return unique;
48
+ });
49
+ if (!eligible.length)
50
+ return { outcome: { outcome: 'cancelled' } };
51
+ const presented = { ...request, options: eligible };
52
+ const token = this.turnToken;
53
+ const id = this.sessionId;
54
+ let outcome = 'unavailable';
55
+ const approvalSignal = this.turnSignal ? AbortSignal.any([this.turnSignal, rpcSignal]) : rpcSignal;
56
+ const broker = this.hooks.broker;
57
+ let generation;
58
+ // A transport disconnect withdraws only the UI question, not the ACP RPC.
59
+ // Late UI answers lose their lease; a fresh online attachment asks again.
60
+ try {
61
+ while (!approvalSignal.aborted) {
62
+ await broker?.waitOnline(approvalSignal);
63
+ generation = broker?.generation;
64
+ const lease = broker?.signal;
65
+ const signal = lease ? AbortSignal.any([approvalSignal, lease]) : approvalSignal;
66
+ const interrupted = Promise.withResolvers();
67
+ const abort = () => interrupted.resolve('cancelled');
68
+ signal.addEventListener('abort', abort, { once: true });
69
+ try {
70
+ outcome = await Promise.race([this.hooks.approve?.(presented, signal) ?? Promise.resolve('unavailable'), interrupted.promise]);
71
+ }
72
+ finally {
73
+ signal.removeEventListener('abort', abort);
74
+ }
75
+ if (broker && !approvalSignal.aborted && (generation !== broker.generation || lease?.aborted))
76
+ continue;
77
+ break;
78
+ }
79
+ }
80
+ catch {
81
+ this.hooks.onNeed?.({ code: 'APPROVAL_FAILED', message: '审批渠道失败或连接已结束;此动作未获授权', requires: '可用的 DSH 审批渠道和原连接' });
82
+ }
83
+ const kind = outcome === 'allowed-once' ? 'allow_once' : outcome === 'rejected' ? 'reject_once' : undefined;
84
+ const option = kind && eligible.find(item => item.kind === kind);
85
+ if (this.busy && token === this.turnToken && id === this.sessionId && !approvalSignal.aborted && option && (!broker || (broker.online && generation === broker.generation))) {
86
+ if (broker)
87
+ broker.authorizeResponse(requestId, generation);
88
+ return { outcome: { outcome: 'selected', optionId: option.optionId } };
89
+ }
90
+ this.hooks.onNeed?.({ code: outcome === 'allowed-once' ? 'APPROVAL_OPTION_UNSUPPORTED' : 'APPROVAL_UNAVAILABLE',
91
+ message: '此动作未获授权;审批已取消、不可用或没有可映射的单次选项', requires: '本次动作的有效审批' });
92
+ return { outcome: { outcome: 'cancelled' } };
93
+ })
94
+ .onRequest(acp.methods.client.elicitation.create, () => {
95
+ this.hooks.onNeed?.({ code: 'INTERACTION_UNSUPPORTED', message: '首版不支持:远端提问或信息收集;已取消,未提交答案', requires: '可承载此提问的官方交互接口' });
96
+ return { action: 'cancel' };
97
+ })
98
+ .onNotification(acp.methods.client.elicitation.complete, () => {
99
+ this.hooks.onNeed?.({ code: 'INTERACTION_UNSUPPORTED', message: '首版不支持:远端提问完成通知;未提交答案', requires: '可承载此提问的官方交互接口' });
100
+ })
101
+ .onRequest('*', () => {
102
+ this.hooks.onNeed?.({ code: 'ACP_EXTENSION_UNSUPPORTED', message: '首版不支持:远端请求的扩展交互;未授权此动作', requires: '受支持的公开 ACP 交互接口' });
103
+ throw new acp.RequestError(-32601, '不支持此扩展交互');
104
+ })
105
+ .onNotification('*', () => {
106
+ this.hooks.onNeed?.({ code: 'ACP_EXTENSION_UNSUPPORTED', message: '首版不支持:收到未知的远端扩展通知', requires: '受支持的公开 ACP 通知接口' });
107
+ });
108
+ if ('readable' in transport && 'writable' in transport)
109
+ this.connection = app.connect(this.observe(transport));
110
+ else {
111
+ // Stable and unstable apps use distinct private symbols. Bridge only their
112
+ // public stream interfaces so either SDK entry point remains supported.
113
+ const outward = new TransformStream();
114
+ const inward = new TransformStream();
115
+ const peer = transport.connect({ readable: outward.readable, writable: inward.writable });
116
+ this.connection = app.connect(this.observe({ readable: inward.readable, writable: outward.writable }));
117
+ void this.connection.closed.then(() => peer.close());
118
+ void peer.closed.then(() => this.connection.close());
119
+ }
120
+ }
121
+ observe(stream) {
122
+ // Observe already-parsed public Stream messages: no NDJSON parsing, second
123
+ // byte buffer, message-size policy, or transport fallback is introduced.
124
+ const readable = stream.readable.pipeThrough(new TransformStream({ transform: (message, controller) => {
125
+ const raw = record(message);
126
+ const params = record(raw?.params);
127
+ const update = record(params?.update);
128
+ if (raw?.method === acp.methods.client.session.update && typeof update?.sessionUpdate === 'string' && !knownUpdates.has(update.sessionUpdate)) {
129
+ if (this.busy && params?.sessionId === this.sessionId)
130
+ this.hooks.onNeed?.({ code: 'ACP_UPDATE_UNSUPPORTED', message: `首版不支持:远端事件 ${safeLabel(update.sessionUpdate)};未展开未知事件内容`, requires: '对应的公开 ACP 显示映射' });
131
+ return;
132
+ }
133
+ controller.enqueue(message);
134
+ } }));
135
+ return { readable, writable: stream.writable };
136
+ }
137
+ get id() { return this.sessionId; }
138
+ get capabilities() { return this.negotiated; }
139
+ get startMethod() { return this.method; }
140
+ validatePrompt(prompt) {
141
+ const capability = this.negotiated.promptCapabilities;
142
+ for (const block of prompt)
143
+ if ((block.type === 'image' && !capability?.image) || (block.type === 'audio' && !capability?.audio) || (block.type === 'resource' && !capability?.embeddedContext))
144
+ throw new NeedError('PROMPT_CONTENT_UNSUPPORTED', `远端未声明此输入内容能力:${block.type};本轮未发送`, '对应内容类型的公开 ACP 输入能力');
145
+ }
146
+ validateContext(context) {
147
+ if ((context.systemPrompt ?? '') === this.systemPrompt)
148
+ return;
149
+ if (!this.negotiated.sessionCapabilities?.systemPrompt)
150
+ throw new NeedError('SYSTEM_PROMPT_UNSUPPORTED', '远端未声明系统与开发者上下文更新能力,不能丢弃这些要求继续', '支持 systemPrompt 的公开 ACP 能力');
151
+ if (!this.negotiated.sessionCapabilities?.resume && !this.negotiated.loadSession)
152
+ throw new NeedError('SYSTEM_PROMPT_UPDATE_UNSUPPORTED', '远端没有可更新原会话上下文的恢复或加载能力', '支持带 systemPrompt 的 session/resume 或 session/load');
153
+ }
154
+ setHooks(hooks) {
155
+ if (this.busy || this.updating)
156
+ throw new NeedError('TURN_BUSY', '上一轮或上下文更新结束前不能更换会话钩子', '上一轮结束');
157
+ this.hooks = hooks;
158
+ }
159
+ get closed() { return this.connection.closed; }
160
+ async start(cwd, resumeId, signal, context) {
161
+ if (this.startAttempted)
162
+ throw new NeedError('SESSION_ALREADY_STARTED', '此连接已用于建立会话,不能转用工作区或身份', '新的独立远端连接');
163
+ this.startAttempted = true;
164
+ const abort = () => this.connection.close(new NeedError('ABORTED', '会话建立已取消', '重新连接主机'));
165
+ signal?.throwIfAborted();
166
+ signal?.addEventListener('abort', abort, { once: true });
167
+ try {
168
+ const hello = await this.connection.agent.request(acp.methods.agent.initialize, {
169
+ protocolVersion: acp.PROTOCOL_VERSION, clientInfo: { name: '@ai-workbench/remote-agent', version: '0.8.0' },
170
+ clientCapabilities: { fs: { readTextFile: false, writeTextFile: false }, terminal: false, session: { notices: {} } },
171
+ }, { cancellationSignal: signal });
172
+ this.hooks.broker?.acknowledge(acp.methods.agent.initialize);
173
+ if (hello.protocolVersion !== acp.PROTOCOL_VERSION)
174
+ throw new NeedError('ACP_VERSION_MISMATCH', 'ACP 协议版本不兼容', '支持 ACP v1 的远端 agent');
175
+ this.negotiated = hello.agentCapabilities ?? {};
176
+ const systemPrompt = context?.systemPrompt;
177
+ if (systemPrompt && !this.negotiated.sessionCapabilities?.systemPrompt) {
178
+ throw new NeedError('SYSTEM_PROMPT_UNSUPPORTED', '远端未声明系统与开发者上下文能力,不能丢弃这些要求继续', '支持 systemPrompt 的公开 ACP 能力');
179
+ }
180
+ const params = { cwd, mcpServers: [], ...(systemPrompt ? { systemPrompt: { mode: 'append', content: [{ type: 'text', text: systemPrompt }] } } : {}) };
181
+ if (resumeId) {
182
+ this.sessionId = resumeId;
183
+ if (this.negotiated.sessionCapabilities?.resume) {
184
+ await this.connection.agent.request(acp.methods.agent.session.resume, { ...params, sessionId: resumeId }, { cancellationSignal: signal });
185
+ this.method = acp.methods.agent.session.resume;
186
+ }
187
+ else {
188
+ if (!this.negotiated.loadSession)
189
+ throw new NeedError('RESUME_UNSUPPORTED', '远端未声明会话恢复或加载能力,不能恢复原会话', '支持 session/resume 或 session/load 的 agent');
190
+ await this.connection.agent.request(acp.methods.agent.session.load, { ...params, sessionId: resumeId }, { cancellationSignal: signal });
191
+ this.method = acp.methods.agent.session.load;
192
+ }
193
+ }
194
+ else {
195
+ const result = await this.connection.agent.request(acp.methods.agent.session.new, params, { cancellationSignal: signal });
196
+ this.sessionId = result.sessionId;
197
+ this.method = acp.methods.agent.session.new;
198
+ }
199
+ this.hooks.broker?.acknowledge(this.method);
200
+ this.ready = true;
201
+ this.cwd = cwd;
202
+ this.systemPrompt = systemPrompt ?? '';
203
+ return this.sessionId;
204
+ }
205
+ catch (error) {
206
+ if (signal?.aborted)
207
+ throw new NeedError('ABORTED', '会话建立已取消;尚无本轮输出', '重新连接主机');
208
+ throw acpNeed(error);
209
+ }
210
+ finally {
211
+ signal?.removeEventListener('abort', abort);
212
+ }
213
+ }
214
+ async updateContext(context, signal) {
215
+ if (!this.ready || !this.sessionId || !this.cwd)
216
+ throw new NeedError('SESSION_REQUIRED', '我需要先建立远端会话', 'ACP 会话');
217
+ if (this.busy || this.updating)
218
+ throw new NeedError('TURN_BUSY', '此远端会话正在处理上一轮或更新上下文', '上一轮结束');
219
+ const text = context.systemPrompt ?? '';
220
+ if (text === this.systemPrompt)
221
+ return undefined;
222
+ this.validateContext(context);
223
+ const resume = !!this.negotiated.sessionCapabilities?.resume;
224
+ signal?.throwIfAborted();
225
+ this.updating = true;
226
+ this.ready = false;
227
+ const abort = () => this.connection.close(new NeedError('ABORTED', '上下文更新已取消,原会话状态需确认', '重新连接原会话'));
228
+ signal?.addEventListener('abort', abort, { once: true });
229
+ try {
230
+ const method = resume ? acp.methods.agent.session.resume : acp.methods.agent.session.load;
231
+ const params = { sessionId: this.sessionId, cwd: this.cwd, mcpServers: [], systemPrompt: { mode: 'append', content: [{ type: 'text', text }] } };
232
+ await this.connection.agent.request(method, params, { cancellationSignal: signal });
233
+ this.hooks.broker?.acknowledge(method);
234
+ this.systemPrompt = text;
235
+ this.ready = true;
236
+ this.hooks.onNeed?.({ code: 'SYSTEM_PROMPT_UPDATE_DEFERRED', message: '系统与开发者上下文变更已通过原会话恢复接口传送。Claude 恢复会话沿用首次系统提示,新要求在下次上下文整理后生效;不能据此认定本轮已立即生效。', requires: '远端实际应用新要求;Claude 须完成下一次上下文整理' });
237
+ return method;
238
+ }
239
+ catch (error) {
240
+ if (signal?.aborted)
241
+ throw new NeedError('ABORTED', '上下文更新已取消;原会话状态需确认', '重新连接原会话');
242
+ throw acpNeed(error);
243
+ }
244
+ finally {
245
+ this.updating = false;
246
+ signal?.removeEventListener('abort', abort);
247
+ }
248
+ }
249
+ async prompt(input, signal) {
250
+ if (!this.ready || !this.sessionId)
251
+ throw new NeedError('SESSION_REQUIRED', '我需要先建立远端会话', 'ACP 会话');
252
+ if (this.busy || this.updating)
253
+ throw new NeedError('TURN_BUSY', '此远端会话正在处理上一轮', '上一轮结束');
254
+ signal?.throwIfAborted();
255
+ const prompt = typeof input === 'string' ? [{ type: 'text', text: input }] : input;
256
+ this.validatePrompt(prompt);
257
+ this.busy = true;
258
+ this.turnSignal = signal;
259
+ this.turnToken = Symbol('turn');
260
+ let rejectAbort;
261
+ const interrupted = new Promise((_resolve, reject) => { rejectAbort = reject; });
262
+ const cancel = () => {
263
+ rejectAbort?.(new NeedError('ABORTED', '已请求取消本轮,远端状态需确认', '确认主机取消状态'));
264
+ void this.connection.agent.notify(acp.methods.agent.session.cancel, { sessionId: this.sessionId }).catch(() => {
265
+ this.hooks.onNeed?.({ code: 'CANCEL_UNCONFIRMED', message: '连接中断,无法确认主机已取消本轮', requires: '恢复连接以确认远端状态' });
266
+ });
267
+ };
268
+ signal?.addEventListener('abort', cancel, { once: true });
269
+ try {
270
+ const request = this.connection.agent.request(acp.methods.agent.session.prompt, { sessionId: this.sessionId, prompt }, { cancellationSignal: signal });
271
+ const result = await (signal ? Promise.race([request, interrupted]) : request);
272
+ this.hooks.broker?.acknowledge(acp.methods.agent.session.prompt);
273
+ signal?.throwIfAborted();
274
+ this.hooks.broker?.completePrompt();
275
+ this.hooks.onResponse?.(result);
276
+ return result.stopReason;
277
+ }
278
+ catch (error) {
279
+ if (signal?.aborted)
280
+ throw new NeedError('ABORTED', '本轮已取消;已请求主机停止', '确认主机取消状态');
281
+ throw acpNeed(error);
282
+ }
283
+ finally {
284
+ signal?.removeEventListener('abort', cancel);
285
+ this.turnSignal = undefined;
286
+ this.busy = false;
287
+ this.turnToken = undefined;
288
+ }
289
+ }
290
+ close() { this.connection.close(); }
291
+ }
292
+ export function acpNeed(error) {
293
+ if (error instanceof NeedError)
294
+ return error;
295
+ if (error instanceof acp.RequestError && error.code === -32000)
296
+ return new NeedError('LOGIN_REQUIRED', '需要登录,请到设置 → 远程主机连接 点登录', '该员工自己的远端模型登录');
297
+ if (error instanceof acp.RequestError)
298
+ return new NeedError('ACP_FAILED', '远端 ACP 请求失败,根因未查明;未切换到新会话', `有效的远端 ACP 连接;RPC 错误码 ${error.code}`);
299
+ return new NeedError('ACP_FAILED', '远端 ACP 请求失败或连接中断,根因未查明;未切换到新会话', '有效的远端 ACP 连接');
300
+ }
301
+ function record(value) { return value !== null && typeof value === 'object' && !Array.isArray(value) ? value : undefined; }
302
+ const knownUpdates = new Set(['user_message_chunk', 'agent_message_chunk', 'agent_thought_chunk', 'tool_call', 'tool_call_update', 'plan', 'plan_update', 'plan_removed', 'available_commands_update', 'current_mode_update', 'config_option_update', 'session_info_update', 'usage_update', 'notice', 'compaction_update', 'compaction_summary_chunk', 'subagent_update', 'session_message', 'session_message_chunk']);
303
+ function safeLabel(value) { return value.replaceAll(/[\r\n\x00-\x1f\x7f]/g, ' '); }
@@ -0,0 +1,15 @@
1
+ /** Provider identities and protected commands from L/S's public contracts. */
2
+ export interface AgentDefinition {
3
+ readonly id: string;
4
+ readonly name: Readonly<{
5
+ zh: string;
6
+ en: string;
7
+ }>;
8
+ readonly command: string;
9
+ readonly loginProvider: 'claude' | 'codex';
10
+ readonly oneShot: boolean;
11
+ }
12
+ export declare const CODEX_AGENT: AgentDefinition;
13
+ export declare const CLAUDE_AGENT: AgentDefinition;
14
+ export declare const AGENTS: readonly AgentDefinition[];
15
+ export declare function agentForProvider(id: string): AgentDefinition | undefined;
package/dist/agents.js ADDED
@@ -0,0 +1,4 @@
1
+ export const CODEX_AGENT = Object.freeze({ id: 'codex-remote', name: Object.freeze({ zh: 'Codex(远程主机)', en: 'Codex (Remote Host)' }), command: '/usr/local/aw-host/bin/codex-acp', loginProvider: 'codex', oneShot: true });
2
+ export const CLAUDE_AGENT = Object.freeze({ id: 'claude-code-workbench', name: Object.freeze({ zh: 'Claude(远程主机)', en: 'Claude (Remote Host)' }), command: '/usr/local/aw-host/bin/claude-agent-acp', loginProvider: 'claude', oneShot: false });
3
+ export const AGENTS = Object.freeze([CODEX_AGENT, CLAUDE_AGENT]);
4
+ export function agentForProvider(id) { return AGENTS.find(agent => agent.id === id); }
@@ -0,0 +1,67 @@
1
+ import { type ChildProcessWithoutNullStreams } from 'node:child_process';
2
+ import { NeedError, type Need } from './remote.ts';
3
+ import type { Interaction } from './interaction.ts';
4
+ import { type ConnectApi, type ConnectTarget, type Project, type Runtime, type RuntimeRequest } from './provider.ts';
5
+ export type Attachment = ChildProcessWithoutNullStreams;
6
+ export interface BrokerOptions {
7
+ maxFrameBytes: number;
8
+ attach: (after: number) => Promise<Attachment>;
9
+ onNeed?: (need: Need) => void;
10
+ onDisconnect?: () => Promise<void>;
11
+ }
12
+ /** One byte stream / SDK app, multiple SSH attachments. Input is never replayed. */
13
+ export declare class BrokerTransport {
14
+ readonly readable: ReadableStream<Uint8Array>;
15
+ readonly writable: WritableStream<Uint8Array>;
16
+ private options;
17
+ private output;
18
+ private attachment?;
19
+ private cursor;
20
+ private epoch;
21
+ private attached;
22
+ private ended;
23
+ private joining?;
24
+ private lease;
25
+ private listeners;
26
+ private permits;
27
+ private approvalSent;
28
+ private unconfirmed;
29
+ private failure?;
30
+ constructor(options: BrokerOptions);
31
+ get online(): boolean;
32
+ get generation(): number;
33
+ get signal(): AbortSignal;
34
+ get lastSequence(): number;
35
+ get error(): NeedError | undefined;
36
+ authorizeResponse(id: string | number | null, generation: number): void;
37
+ /** Called only by the ACP protocol layer after a matching result or current-turn output. */
38
+ acknowledge(method: string): void;
39
+ /** A matching terminal prompt response settles all approval replies in that turn. */
40
+ completePrompt(): void;
41
+ onChange(fn: () => void): () => void;
42
+ private changed;
43
+ private notice;
44
+ fail(error: NeedError): void;
45
+ close(): void;
46
+ waitOnline(signal?: AbortSignal): Promise<void>;
47
+ connect(): Promise<void>;
48
+ private attach;
49
+ private write;
50
+ }
51
+ export type SshArgv = (target: ConnectTarget) => string[];
52
+ export interface BrokerIo {
53
+ run: (command: string, signal?: AbortSignal) => Promise<string>;
54
+ attach: (command: string) => Promise<Attachment>;
55
+ }
56
+ export declare function controlNeed(stderr: string): NeedError;
57
+ export declare function sshIo(target: ConnectTarget, argv: SshArgv, maxControlBytes: number): BrokerIo;
58
+ export declare function brokerCommand(t: ConnectTarget, args: string[]): string;
59
+ export declare function brokerId(sessionId: string, agentId?: string): string;
60
+ export interface BrokerSettings {
61
+ brokerMaxFrameBytes?: number;
62
+ brokerControlMaxBytes?: number;
63
+ }
64
+ export declare function brokerFactory(get: () => ConnectApi | undefined, _ask: Interaction | undefined, configuration: BrokerSettings | (() => BrokerSettings), provided?: {
65
+ argv?: SshArgv;
66
+ io?: (target: ConnectTarget) => BrokerIo;
67
+ }): (target: ConnectTarget, project: Project, request: RuntimeRequest) => Promise<Runtime>;