@ai-workbench/remote-agent 0.8.28 → 0.9.0-toolbridge.2
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 +13 -3
- package/dist/acp.d.ts +3 -0
- package/dist/acp.js +35 -7
- package/dist/agents.js +1 -1
- package/dist/broker-ownership.d.ts +10 -0
- package/dist/broker-ownership.js +59 -0
- package/dist/broker-runtime.js +5 -2
- package/dist/claude-local.js +2 -1
- package/dist/employee-mcp.d.ts +2 -0
- package/dist/employee-mcp.js +3 -1
- package/dist/index.d.ts +8 -3
- package/dist/index.js +108 -16
- package/dist/presentation.js +3 -0
- package/dist/provider.d.ts +4 -0
- package/dist/toolbridge-loop.d.ts +19 -0
- package/dist/toolbridge-loop.js +251 -0
- package/dist/toolbridge-table.d.ts +10 -0
- package/dist/toolbridge-table.js +27 -0
- package/dist/toolbridge.d.ts +114 -0
- package/dist/toolbridge.js +205 -0
- package/package.json +6 -5
package/README.md
CHANGED
|
@@ -1,10 +1,20 @@
|
|
|
1
|
-
# AI Workbench 远程助手 · @ai-workbench/remote-agent@0.
|
|
1
|
+
# AI Workbench 远程助手 · @ai-workbench/remote-agent@0.9.0-toolbridge.2 · 插件 → 添加插件
|
|
2
|
+
|
|
3
|
+
## 含工具桥预发布线
|
|
4
|
+
|
|
5
|
+
本包为 `0.9.0-toolbridge.2`,在独立 `toolbridge` 分支开发;稳定 main/0.8.x 和原总包保持原样。员工安装由连接组件提供的「AI 写作工作台(含工具桥)」总包,与不含工具桥的版本二选一。预发布渠道为 `toolbridge`,本轮仅提供本地固定包,不发布 npm。
|
|
6
|
+
|
|
7
|
+
选择本机文件夹后,Claude 和 Codex 可以把 DSH 已启用的 MCP、web、skill、ask_user、todo/present/goal 等工具交给 DSH 原生执行和审批。内置文件/命令、子代理/agent-team、run_code 不在桥接表;原有文件和命令执行方式保留。plan 不自动放行桥接权限;default 下 Claude 的桥接原生权限由插件单次应答,员工只在 DSH 审批实际工具。主机文件夹不开放工具桥。
|
|
8
|
+
|
|
9
|
+
工具表按每次主机连接生成快照,新增/移除在下一次连接生效;不承诺 Codex 当前 MCP 会话热刷新。审批期间的插话等当前主机 prompt 结束后再发,停泊中切换助手会停止原轮次并明确提示。工具主动结束DSH轮次时,已提交结果仍交回原agent;确认原生工具收到后结束原prompt,下一条沿原线程继续,不在已关闭步骤追加模型回复。取消和主机结束等待后的结果不重放;工具已执行的副作用不能因取消而撤销,请按说明核对。图片结果由官方附件组件读取后交回。
|
|
10
|
+
|
|
11
|
+
Codex 需要 S 后续安装的独立受保护命令 `/usr/local/aw-host/bin/codex-acp-toolbridge`(aw.7),不会替换原 codex-acp/aw.6;Claude命令不变。指定桥接 server 的等待为 owner 确认的 **1800 秒**、审批交 DSH;不改其他 server 或全局策略,不新增设置项。N0.4.9、L0.7.37沿既有公开合同;真实界面/员工验收由编排安排。完整合同和证据见 `docs/contracts/codex-toolbridge.md`、`docs/evidence/2026-10-26-toolbridge/REPORT.md`。
|
|
2
12
|
|
|
3
13
|
通过官方 DSH 插件入口使用 Claude(远程主机)与 Codex(远程主机)。登录和工作区来源由“远程主机连接”组件提供;员工在原生工作区和模型菜单中选择文件夹与型号。
|
|
4
14
|
|
|
5
15
|
文件和命令在所选的本机或主机目录执行,订阅凭据留在主机。插件不改写 DSH 的默认模型,不为员工创建同名主机目录,已有主机会话只提取一次原有主机来源并保存。
|
|
6
16
|
|
|
7
|
-
需要远程主机连接组件 0.7.
|
|
17
|
+
需要远程主机连接组件 0.7.37 或更新的 0.7.x、联网助手 0.4.9,以及支持公开会话归属接口的主机组件 S0.4.26。主机来源保存于官方 storage.domain 的会话元数据表,每轮仅由连接组件检查一次主机目录,不检查本机占位目录。
|
|
8
18
|
|
|
9
19
|
Claude 本机 Bash 会单独消费员工桥代理的控制诊断,不把它交给模型当作命令输出;真实传输中断仍提示结果未知。命令自身的标准输出、标准错误与退出码照常返回。
|
|
10
20
|
|
|
@@ -16,7 +26,7 @@ Claude 本机 Bash 会单独消费员工桥代理的控制诊断,不把它交
|
|
|
16
26
|
|
|
17
27
|
同一 DSH 会话切换 Claude/Codex 时,切回会接本品牌最近的远端会话,并补充它尚未见过的可见对话;首次使用某品牌则带入全部可见历史。工具内部状态、推理和技术详情不转交。历史超出既有 `acpMaxMessageBytes` 容量时,只带能容纳的最近完整轮,并在技术详情说明省略数量;当前输入保持完整,超限仍明确失败。没有新增设置、重试或时限。
|
|
18
28
|
|
|
19
|
-
|
|
29
|
+
断线会结束本轮;已派发的操作可能已部分执行,请先检查文件。恢复连接后可在原会话继续。旧进程清理和技术详情记录尽力而为,不阻止发起下一轮。续接明确遇到原线程被占用时,使用 S0.4.26 的会话归属和原子结束接口:只有唯一匹配的旧进程确实已断开、主机明确确认退出后,才接回同一线程一次;不会新建替代线程、重发用户输入或抢占活跃会话。旧版无归属进程不能安全清理,需等待主机按现有策略回收;失败或释放结果未知则说明本轮输入未发送。原生本机会话恢复的兼容性及实际设备结果见本轮交付证据,未验证项不作为已通过。
|
|
20
30
|
|
|
21
31
|
DSH 启动和打开模型菜单时读取一次主机型号列表。发送时不再查询目录或预核所选型号;所选型号不可用时,请在模型菜单重新选择。
|
|
22
32
|
|
package/dist/acp.d.ts
CHANGED
|
@@ -6,6 +6,7 @@ import type { BrokerTransport } from './broker.ts';
|
|
|
6
6
|
import { type SanitizedObservation } from './conversation-details.ts';
|
|
7
7
|
import { type ModelConfigurationSnapshot } from './model-configuration.ts';
|
|
8
8
|
import type { EmployeeRouting } from './employee-relay.ts';
|
|
9
|
+
import type { SessionOwnership } from './broker-ownership.ts';
|
|
9
10
|
export type ApprovalOutcome = 'allowed-once' | 'rejected' | 'cancelled' | 'unavailable';
|
|
10
11
|
export interface AcpHooks {
|
|
11
12
|
closeDiagnostics?: CloseDiagnostics;
|
|
@@ -25,6 +26,7 @@ export interface AcpHooks {
|
|
|
25
26
|
onEmployeeNotice?: (notice: string) => void;
|
|
26
27
|
onDetail?: (detail: Readonly<SanitizedObservation>) => void;
|
|
27
28
|
broker?: BrokerTransport;
|
|
29
|
+
sessionOwnership?: SessionOwnership;
|
|
28
30
|
employeeExecution?: (params: unknown, signal?: AbortSignal) => Promise<Record<string, unknown>>;
|
|
29
31
|
}
|
|
30
32
|
export interface EmployeeExecutionMeta {
|
|
@@ -35,6 +37,7 @@ export interface EmployeeExecutionMeta {
|
|
|
35
37
|
acpMaxMessageBytes: number;
|
|
36
38
|
}
|
|
37
39
|
export interface AcpStartContext {
|
|
40
|
+
toolBridgeServers?: EmployeeRouting['mcpServers'];
|
|
38
41
|
systemPrompt?: string;
|
|
39
42
|
codexNewSessionOnly?: true;
|
|
40
43
|
employeeExecution?: EmployeeExecutionMeta;
|
package/dist/acp.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { employeeExecutionInfo, employeeMcpSummary, employeeHttpNotice } from "./employee-info.js";
|
|
2
2
|
import { employeeAdapterCodes, employeeOpenCauseCodes } from "./employee-reasons.js";
|
|
3
|
-
import { CloseDiagnostics, withFirstFailureEvidence } from "./close-diagnostics.js";
|
|
3
|
+
import { CloseDiagnostics, closeCode, withFirstFailureEvidence } from "./close-diagnostics.js";
|
|
4
4
|
import * as acp from '@agentclientprotocol/sdk/unstable';
|
|
5
5
|
import { NeedError } from "./remote.js";
|
|
6
6
|
import { employeeFailure } from "./presentation.js";
|
|
@@ -227,6 +227,9 @@ export class AcpSession {
|
|
|
227
227
|
employeeNoticeNames = new Set();
|
|
228
228
|
employeeNoticeUnshown = false;
|
|
229
229
|
employeeInfo(value) {
|
|
230
|
+
const bridge = record(record(value)?.mcpBridge);
|
|
231
|
+
if (bridge)
|
|
232
|
+
this.detail({ kind: 'session', severity: 'info', code: 'TOOLBRIDGE_HOST_POLICY', summary: '主机回报本轮指定桥接server的等待与审批策略;不改变其他工具。', payload: { toolTimeoutSec: typeof bridge.toolTimeoutSec === 'number' && Number.isFinite(bridge.toolTimeoutSec) ? bridge.toolTimeoutSec : null, approval: bridge.approval === 'approve' ? 'approve' : 'UNKNOWN', source: bridge.source === 'owner-2026-10-26' ? bridge.source : 'UNKNOWN' } });
|
|
230
233
|
const info = employeeExecutionInfo(value);
|
|
231
234
|
if (!info)
|
|
232
235
|
return;
|
|
@@ -484,15 +487,15 @@ export class AcpSession {
|
|
|
484
487
|
signal?.addEventListener('abort', abort, { once: true });
|
|
485
488
|
try {
|
|
486
489
|
const hello = await this.stage('acp-initialize', () => this.connection.agent.request(acp.methods.agent.initialize, {
|
|
487
|
-
protocolVersion: acp.PROTOCOL_VERSION, clientInfo: { name: '@ai-workbench/remote-agent', version: '0.
|
|
488
|
-
clientCapabilities: { fs: { readTextFile: false, writeTextFile: false }, terminal: false, session: { notices: {} }, ...(this.employeeMeta ? { _meta: { awEmployeeExecution: { version: 1, closeReason: 1 } } } : {}) },
|
|
490
|
+
protocolVersion: acp.PROTOCOL_VERSION, clientInfo: { name: '@ai-workbench/remote-agent', version: '0.9.0-toolbridge.2' },
|
|
491
|
+
clientCapabilities: { fs: { readTextFile: false, writeTextFile: false }, terminal: false, session: { notices: {} }, ...(this.employeeMeta ? { _meta: { awEmployeeExecution: { version: 1, closeReason: 1, ...(context?.toolBridgeServers ? { mcpBridge: 1 } : {}) } } } : {}) },
|
|
489
492
|
}, { cancellationSignal: signal }));
|
|
490
493
|
if (hello.protocolVersion !== acp.PROTOCOL_VERSION)
|
|
491
494
|
throw new NeedError('ACP_VERSION_MISMATCH', 'ACP 协议版本不兼容', '支持 ACP v1 的远端 agent');
|
|
492
495
|
this.negotiated = hello.agentCapabilities ?? {};
|
|
493
496
|
if (this.employeeMeta) {
|
|
494
497
|
const policy = record(hello._meta?.awEmployeeExecution);
|
|
495
|
-
if (!this.hooks.employeeExecution || !policy || Object.keys(policy).length !== 5 || policy.version !== 1 || policy.nativeCliVersion !== '0.160.1' || policy.native !== true || policy.perRpcGate !== true || policy.noHostFallback !== true)
|
|
498
|
+
if (!this.hooks.employeeExecution || !policy || Object.keys(policy).length !== (context?.toolBridgeServers ? 6 : 5) || context?.toolBridgeServers && policy.mcpBridge !== 1 || policy.version !== 1 || policy.nativeCliVersion !== '0.160.1' || policy.native !== true || policy.perRpcGate !== true || policy.noHostFallback !== true)
|
|
496
499
|
throw new NeedError('EMPLOYEE_EXECUTION_UNSUPPORTED', '当前主机尚不支持在你的电脑执行Codex原生工具,本轮没有执行。', 'Codex员工端原生执行适配器与明确能力声明');
|
|
497
500
|
this.employeeNegotiated = true;
|
|
498
501
|
this.detail({ kind: 'session', severity: 'info', code: 'EMPLOYEE_EXECUTION_SELECTED', summary: '文件和命令在员工电脑的选定目录执行;订阅凭据留在主机,本机扩展的忽略详情单独记录。', payload: { bindingId: this.employeeMeta.bindingId, workspaceId: this.employeeMeta.workspaceId, cwd: this.employeeMeta.cwd, nativeCliVersion: '0.160.1', noHostFallback: true, agentConfigSource: 'host CODEX_HOME', unsupportedLocalFeatures: ['http-mcp-execution', 'hooks', 'plugins', 'cold-resume', 'slash-commands', 'host-file-reports'] } });
|
|
@@ -503,19 +506,43 @@ export class AcpSession {
|
|
|
503
506
|
this.validatePrompt(prompt);
|
|
504
507
|
const routing = context?.employeeRouting ? structuredClone(context.employeeRouting) : undefined;
|
|
505
508
|
this.employeeRouting = routing;
|
|
506
|
-
const params = { cwd, mcpServers: routing ? structuredClone(routing.mcpServers) : [], ...((routing || this.employeeMeta) ? { _meta: { ...(this.employeeMeta ? { awEmployeeExecution: this.employeeMeta } : {}), ...(routing ? { claudeCode: { options: employeeClaudeOptions(routing) } } : {}) } } : {}), ...(systemPrompt && !(resumeId && this.codexNewSessionOnly) ? { systemPrompt: { mode: 'append', content: [{ type: 'text', text: systemPrompt }] } } : {}) };
|
|
509
|
+
const params = { cwd, mcpServers: context?.toolBridgeServers ? structuredClone(context.toolBridgeServers) : routing ? structuredClone(routing.mcpServers) : [], ...((routing || this.employeeMeta) ? { _meta: { ...(this.employeeMeta ? { awEmployeeExecution: this.employeeMeta } : {}), ...(routing ? { claudeCode: { options: employeeClaudeOptions(routing) } } : {}) } } : {}), ...(systemPrompt && !(resumeId && this.codexNewSessionOnly) ? { systemPrompt: { mode: 'append', content: [{ type: 'text', text: systemPrompt }] } } : {}) };
|
|
507
510
|
const configRevision = this.resetConfiguration();
|
|
511
|
+
const restore = async (operation) => {
|
|
512
|
+
try {
|
|
513
|
+
return await operation();
|
|
514
|
+
}
|
|
515
|
+
catch (error) {
|
|
516
|
+
if (!resumeId || acpNeed(error).code !== 'REMOTE_SESSION_BUSY' || !this.hooks.sessionOwnership)
|
|
517
|
+
throw error;
|
|
518
|
+
this.detail({ kind: 'session', severity: 'info', code: 'REMOTE_SESSION_BUSY', summary: '原远端会话被占用,正在核对同一会话与线程的已断开旧进程;尚未发送用户输入。' });
|
|
519
|
+
try {
|
|
520
|
+
await this.hooks.sessionOwnership.stopDetached(resumeId, signal);
|
|
521
|
+
signal?.throwIfAborted();
|
|
522
|
+
}
|
|
523
|
+
catch (failure) {
|
|
524
|
+
this.detail({ kind: 'session', severity: 'warning', code: 'REMOTE_SESSION_RELEASE_UNCONFIRMED', summary: '旧会话释放未获确认,本轮没有再次接续或发送用户输入。', payload: { reason: failure instanceof NeedError ? failure.requires : closeCode(failure, 'BROKER_CONTROL_FAILED') } });
|
|
525
|
+
throw failure;
|
|
526
|
+
}
|
|
527
|
+
this.detail({ kind: 'session', severity: 'info', code: 'REMOTE_SESSION_RELEASE_CONFIRMED', summary: '主机已确认原会话的已断开旧进程退出,继续接回同一远端会话一次。' });
|
|
528
|
+
return operation(); // Exactly one continuation, only after the explicit release receipt.
|
|
529
|
+
}
|
|
530
|
+
};
|
|
508
531
|
if (resumeId) {
|
|
509
532
|
this.sessionId = resumeId;
|
|
510
533
|
if (this.negotiated.sessionCapabilities?.resume) {
|
|
511
|
-
const result = await this.stage('acp-session-resume', () => this.connection.agent.request(acp.methods.agent.session.resume, { ...params, sessionId: resumeId }, { cancellationSignal: signal }));
|
|
534
|
+
const result = await restore(() => this.stage('acp-session-resume', () => this.connection.agent.request(acp.methods.agent.session.resume, { ...params, sessionId: resumeId }, { cancellationSignal: signal })));
|
|
535
|
+
if (this.employeeMeta)
|
|
536
|
+
this.employeeInfo(result._meta?.awEmployeeExecutionInfo);
|
|
512
537
|
this.lifecycleConfiguration(result, configRevision);
|
|
513
538
|
this.method = acp.methods.agent.session.resume;
|
|
514
539
|
}
|
|
515
540
|
else {
|
|
516
541
|
if (!this.negotiated.loadSession)
|
|
517
542
|
throw new NeedError('RESUME_UNSUPPORTED', '远端未声明会话恢复或加载能力,不能恢复原会话', '支持 session/resume 或 session/load 的 agent');
|
|
518
|
-
const result = await this.stage('acp-session-load', () => this.connection.agent.request(acp.methods.agent.session.load, { ...params, sessionId: resumeId }, { cancellationSignal: signal }));
|
|
543
|
+
const result = await restore(() => this.stage('acp-session-load', () => this.connection.agent.request(acp.methods.agent.session.load, { ...params, sessionId: resumeId }, { cancellationSignal: signal })));
|
|
544
|
+
if (this.employeeMeta)
|
|
545
|
+
this.employeeInfo(result._meta?.awEmployeeExecutionInfo);
|
|
519
546
|
this.lifecycleConfiguration(result, configRevision);
|
|
520
547
|
this.method = acp.methods.agent.session.load;
|
|
521
548
|
}
|
|
@@ -528,6 +555,7 @@ export class AcpSession {
|
|
|
528
555
|
this.lifecycleConfiguration(result, configRevision);
|
|
529
556
|
this.method = acp.methods.agent.session.new;
|
|
530
557
|
}
|
|
558
|
+
await this.hooks.sessionOwnership?.bind(this.sessionId, signal);
|
|
531
559
|
this.ready = true;
|
|
532
560
|
this.cwd = cwd;
|
|
533
561
|
this.systemPrompt = systemPrompt ?? '';
|
package/dist/agents.js
CHANGED
|
@@ -1,4 +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' });
|
|
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-toolbridge', loginProvider: 'codex' });
|
|
2
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' });
|
|
3
3
|
export const AGENTS = Object.freeze([CODEX_AGENT, CLAUDE_AGENT]);
|
|
4
4
|
export function agentForProvider(id) { return AGENTS.find(agent => agent.id === id); }
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { type BrokerIo } from './broker.ts';
|
|
2
|
+
export interface SessionOwnership {
|
|
3
|
+
bind(remoteSessionId: string, signal?: AbortSignal): Promise<void>;
|
|
4
|
+
stopDetached(remoteSessionId: string, signal?: AbortSignal): Promise<void>;
|
|
5
|
+
}
|
|
6
|
+
/** S0426 public control only: no process/lock scans or client ownership journal. */
|
|
7
|
+
export declare function brokerOwnership(io: BrokerIo, command: (args: string[]) => string, started: {
|
|
8
|
+
id: string;
|
|
9
|
+
instanceId: string;
|
|
10
|
+
}, receipt: unknown, dshSessionId: string, provider: 'codex' | 'claude'): SessionOwnership;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { controlJson } from "./broker.js";
|
|
2
|
+
import { NeedError } from "./remote.js";
|
|
3
|
+
import { closeCode } from "./close-diagnostics.js";
|
|
4
|
+
const record = (v) => v !== null && typeof v === 'object' && !Array.isArray(v) ? v : undefined;
|
|
5
|
+
const line = (v) => typeof v === 'string' && v.length > 0 && !/[\r\n\0]/.test(v);
|
|
6
|
+
const same = (value, expected) => { const row = record(value); return row?.dshSessionId === expected.dshSessionId && row.provider === expected.provider && row.remoteSessionId === expected.remoteSessionId; };
|
|
7
|
+
const busy = (reason) => new NeedError('REMOTE_SESSION_BUSY', '原会话仍被上一轮占用,本轮输入未发送。', '已断开旧会话的安全释放尚未确认;' + reason + (reason === 'OWNERSHIP_NOT_FOUND_OR_LEGACY' ? ';没有可证明归属的旧会话,不结束任何旧进程,请等待主机按现有策略回收。' : ''));
|
|
8
|
+
/** S0426 public control only: no process/lock scans or client ownership journal. */
|
|
9
|
+
export function brokerOwnership(io, command, started, receipt, dshSessionId, provider) {
|
|
10
|
+
let owner = { dshSessionId, provider, remoteSessionId: null };
|
|
11
|
+
const initial = record(receipt);
|
|
12
|
+
if (!line(dshSessionId) || initial?.ownershipProtocolVersion !== 1 || !same(initial.ownership, owner))
|
|
13
|
+
throw new NeedError('BROKER_OWNERSHIP_UNAVAILABLE', '主机会话缺少归属能力,本轮输入未发送;请联系管理员更新主机组件。', 'S >=0.4.26 / ownershipProtocolVersion:1');
|
|
14
|
+
const flags = (value) => ['--dsh-session-id', value.dshSessionId, '--provider', value.provider, '--remote-session-id', value.remoteSessionId];
|
|
15
|
+
const confirmed = (value, id, instanceId, expected) => value?.id === id && value.instanceId === instanceId && value.ownershipProtocolVersion === 1 && same(value.ownership, expected);
|
|
16
|
+
return {
|
|
17
|
+
async bind(remoteSessionId, signal) {
|
|
18
|
+
signal?.throwIfAborted();
|
|
19
|
+
if (!line(remoteSessionId))
|
|
20
|
+
throw new NeedError('BROKER_OWNERSHIP_INVALID', '远端会话编号无法绑定,本轮输入未发送。', 'S0426单行原生会话编号');
|
|
21
|
+
const next = { ...owner, remoteSessionId };
|
|
22
|
+
const value = record(controlJson(await io.run(command(['bind', started.id, '--if-instance', started.instanceId, ...flags(next), ...(owner.remoteSessionId === null ? [] : ['--if-remote-session-id', owner.remoteSessionId])]), signal)));
|
|
23
|
+
signal?.throwIfAborted();
|
|
24
|
+
if (value?.type !== 'bound' || !confirmed(value, started.id, started.instanceId, next))
|
|
25
|
+
throw new NeedError('BROKER_OWNERSHIP_UNCONFIRMED', '主机未确认会话归属,本轮输入未发送。', 'S0426精确归属绑定回执');
|
|
26
|
+
owner = next;
|
|
27
|
+
},
|
|
28
|
+
async stopDetached(remoteSessionId, signal) {
|
|
29
|
+
try {
|
|
30
|
+
signal?.throwIfAborted();
|
|
31
|
+
if (!line(remoteSessionId))
|
|
32
|
+
throw busy('OWNERSHIP_INVALID');
|
|
33
|
+
const wanted = { ...owner, remoteSessionId };
|
|
34
|
+
const entries = controlJson(await io.run(command(['list']), signal));
|
|
35
|
+
signal?.throwIfAborted();
|
|
36
|
+
if (!Array.isArray(entries) || entries.some(row => !record(row)))
|
|
37
|
+
throw busy('OWNERSHIP_LIST_INVALID');
|
|
38
|
+
const matches = entries.map(record).filter(row => same(row?.ownership, wanted));
|
|
39
|
+
if (matches.length !== 1)
|
|
40
|
+
throw busy(matches.length ? 'OWNERSHIP_AMBIGUOUS' : 'OWNERSHIP_NOT_FOUND_OR_LEGACY');
|
|
41
|
+
const row = matches[0];
|
|
42
|
+
if (row.id === started.id || !line(row.id) || !line(row.instanceId) || row.ownershipProtocolVersion !== 1)
|
|
43
|
+
throw busy('OWNERSHIP_UNAVAILABLE');
|
|
44
|
+
const lifecycle = record(row.lifecycle);
|
|
45
|
+
if (!['running', 'exited'].includes(row.state) || lifecycle?.attached !== false || !line(lifecycle.detachedAt))
|
|
46
|
+
throw busy('SESSION_NOT_DETACHED');
|
|
47
|
+
const value = record(controlJson(await io.run(command(['stop-detached', row.id, '--if-instance', row.instanceId, ...flags(wanted)]), signal)));
|
|
48
|
+
signal?.throwIfAborted();
|
|
49
|
+
if (value?.type !== 'killed' || !confirmed(value, row.id, row.instanceId, wanted) || value.agentExited !== true || value.processGroupExited !== true)
|
|
50
|
+
throw busy('STOP_UNCONFIRMED');
|
|
51
|
+
}
|
|
52
|
+
catch (error) {
|
|
53
|
+
if (error instanceof NeedError && error.code === 'REMOTE_SESSION_BUSY')
|
|
54
|
+
throw error;
|
|
55
|
+
throw busy(closeCode(error, 'BROKER_CONTROL_FAILED'));
|
|
56
|
+
}
|
|
57
|
+
},
|
|
58
|
+
};
|
|
59
|
+
}
|
package/dist/broker-runtime.js
CHANGED
|
@@ -12,6 +12,7 @@ import { BrokerTransport, brokerCommand, brokerId, sshIo, controlJson } from "./
|
|
|
12
12
|
import { CODEX_AGENT } from "./agents.js";
|
|
13
13
|
import { NeedError } from "./remote.js";
|
|
14
14
|
import { identity } from "./provider.js";
|
|
15
|
+
import { brokerOwnership } from "./broker-ownership.js";
|
|
15
16
|
/** Each invocation owns a new agent. Context resumes by native session ID, never by reattaching a broker. */
|
|
16
17
|
export function brokerFactory(_get, _ask, configuration, provided) {
|
|
17
18
|
return async (target, project, request) => {
|
|
@@ -25,7 +26,7 @@ export function brokerFactory(_get, _ask, configuration, provided) {
|
|
|
25
26
|
const command = (args) => brokerCommand(target, args);
|
|
26
27
|
const diagnostics = request.closeDiagnostics ?? new CloseDiagnostics(detail => request.detailChannel?.record(detail));
|
|
27
28
|
const id = brokerId(request.sessionId + '/' + randomUUID(), agent.id);
|
|
28
|
-
const startCommand = command(['start', id, '--cwd', project.path, '--settings', target.session.settingsPath, '--', agent.command]);
|
|
29
|
+
const startCommand = command(['start', id, '--cwd', project.path, '--settings', target.session.settingsPath, '--dsh-session-id', request.sessionId, '--provider', agent.loginProvider, '--', agent.command]);
|
|
29
30
|
// environments.toml overrides CODEX_EXEC_SERVER_URL. Refuse that host default rather
|
|
30
31
|
// than allowing a local-workspace resume to select a host executor.
|
|
31
32
|
const employeeStart = request.employeeScope && agent.loginProvider === 'codex';
|
|
@@ -47,7 +48,9 @@ export function brokerFactory(_get, _ask, configuration, provided) {
|
|
|
47
48
|
};
|
|
48
49
|
const aborted = () => channel.fail(new NeedError('ABORTED', '本轮已取消。', '确认主机状态'), { layer: 'broker', initiator: 'broker.open-signal', reason: 'ABORTED', action: 'abort' });
|
|
49
50
|
request.signal?.addEventListener('abort', aborted, { once: true });
|
|
51
|
+
let sessionOwnership;
|
|
50
52
|
try {
|
|
53
|
+
sessionOwnership = brokerOwnership(io, command, { id, instanceId: started.instanceId }, started, request.sessionId, agent.loginProvider);
|
|
51
54
|
request.signal?.throwIfAborted();
|
|
52
55
|
await channel.connect();
|
|
53
56
|
}
|
|
@@ -58,6 +61,6 @@ export function brokerFactory(_get, _ask, configuration, provided) {
|
|
|
58
61
|
finally {
|
|
59
62
|
request.signal?.removeEventListener('abort', aborted);
|
|
60
63
|
}
|
|
61
|
-
return { identity: identity(target), cwd: project.path, closeDiagnostics: diagnostics, broker: channel, acpTransport: channel, dispose, detach: dispose };
|
|
64
|
+
return { identity: identity(target), cwd: project.path, closeDiagnostics: diagnostics, broker: channel, sessionOwnership, acpTransport: channel, dispose, detach: dispose };
|
|
62
65
|
};
|
|
63
66
|
}
|
package/dist/claude-local.js
CHANGED
|
@@ -115,7 +115,8 @@ export function createClaudeLocalProvider(options) {
|
|
|
115
115
|
const network = options.getNetwork();
|
|
116
116
|
lease = await (options.ports?.lease ?? openEmployeeLease)({ closeDiagnostics, getConnect: () => options.getConnect(), getNetwork: options.getNetwork, readWorkspace: () => options.readWorkspace(request.sessionId), host: target, sessionId: request.sessionId, control: async (_target, command, signal) => (await transport()).run(command, signal), detail: unscoped }, request.signal);
|
|
117
117
|
const shell = (options.ports?.shell ?? createEmployeeShell)({ closeDiagnostics, target: lease.target, sshArgs: lease.sshArgs, maxBytes: maximum, killGraceMs: grace, attach: async (spec) => (await transport()).attach([spec.command, ...spec.args].map(shellQuote).join(' ')) });
|
|
118
|
-
|
|
118
|
+
request.toolBridge?.enable(event => details.record(event));
|
|
119
|
+
mcp = await (options.ports?.mcp ?? openEmployeeMcp)({ bridge: request.toolBridge, closeDiagnostics, lease, maxBytes: maximum, killGraceMs: grace, runShell: shell, activeSignal: () => entry?.active?.signal, captureDetail: () => { const sink = entry?.active?.details; if (!sink)
|
|
119
120
|
throw failure('EMPLOYEE_NO_ACTIVE_TURN'); return observation => sink.record(observation); }, detail: unscoped });
|
|
120
121
|
entry = { runtime: undefined, scope, lease, details, request };
|
|
121
122
|
const row = entry;
|
package/dist/employee-mcp.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { ToolBridge } from './toolbridge.ts';
|
|
1
2
|
import { type CloseCause, type CloseDiagnostics, type CloseObserver } from './close-diagnostics.ts';
|
|
2
3
|
import { type Socket } from 'node:net';
|
|
3
4
|
import { type JSONRPCMessage, type Tool } from '@modelcontextprotocol/sdk/types.js';
|
|
@@ -28,6 +29,7 @@ export declare class EmployeeSocketTransport implements Transport {
|
|
|
28
29
|
close(cause?: CloseCause): Promise<void>;
|
|
29
30
|
}
|
|
30
31
|
export interface EmployeeMcpOptions {
|
|
32
|
+
bridge?: ToolBridge;
|
|
31
33
|
closeDiagnostics?: CloseDiagnostics;
|
|
32
34
|
lease: EmployeeLease;
|
|
33
35
|
maxBytes: number;
|
package/dist/employee-mcp.js
CHANGED
|
@@ -136,12 +136,14 @@ export async function openEmployeeMcp(options) {
|
|
|
136
136
|
const mcp = new Server({ name: 'ai-workbench-employee', version: '0.8.14' }, { capabilities: { tools: {} } }), transport = new EmployeeSocketTransport(socket, options.maxBytes, observeConnection);
|
|
137
137
|
clients.add(mcp);
|
|
138
138
|
transports.add(transport);
|
|
139
|
-
mcp.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: EMPLOYEE_MCP_TOOLS }));
|
|
139
|
+
mcp.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: [...EMPLOYEE_MCP_TOOLS, ...options.bridge?.tools ?? []] }));
|
|
140
140
|
mcp.setRequestHandler(CallToolRequestSchema, async (request, extra) => {
|
|
141
141
|
const active = options.activeSignal();
|
|
142
142
|
if (!active || active.aborted)
|
|
143
143
|
return { isError: true, content: [{ type: 'text', text: 'EMPLOYEE_NO_ACTIVE_TURN:当前没有已接纳的用户轮次,本机工具未执行。' }] };
|
|
144
144
|
const signal = AbortSignal.any([active, extra.signal, transport.signal, options.lease.signal]);
|
|
145
|
+
if (options.bridge?.isBridge(request.params.name))
|
|
146
|
+
return options.bridge.call(request.params.name, request.params.arguments ?? {}, signal);
|
|
145
147
|
let sink;
|
|
146
148
|
try {
|
|
147
149
|
sink = options.captureDetail?.() ?? options.detail;
|
package/dist/index.d.ts
CHANGED
|
@@ -57,6 +57,7 @@ export declare class CodexRemoteAdapter extends LlmAdapter {
|
|
|
57
57
|
private readonly directory?;
|
|
58
58
|
private readonly preflightDiagnostics?;
|
|
59
59
|
private readonly permissions?;
|
|
60
|
+
private bridgeLoop;
|
|
60
61
|
private config;
|
|
61
62
|
private sessions;
|
|
62
63
|
private turns;
|
|
@@ -65,7 +66,8 @@ export declare class CodexRemoteAdapter extends LlmAdapter {
|
|
|
65
66
|
private detached;
|
|
66
67
|
private modelPresentation;
|
|
67
68
|
private employeeNoticeSessions;
|
|
68
|
-
|
|
69
|
+
private pendingEmployeeNotices;
|
|
70
|
+
constructor(config: PluginConfig | (() => PluginConfig), resolveRuntime: RuntimeProvider, approve?: InvocationApproval | undefined, providerName?: () => string, agent?: AgentDefinition, readImage?: ImageReader | undefined, reportLoginFailure?: LoginFailureReporter | undefined, directory?: ModelDirectory | undefined, preflightDiagnostics?: PreflightDiagnostics | undefined, permissions?: PermissionSource | undefined, bridgeStopped?: (sessionId: string, notice: string) => void);
|
|
69
71
|
providerInfo(provider: string): {
|
|
70
72
|
id: string;
|
|
71
73
|
name: string;
|
|
@@ -88,6 +90,8 @@ export declare class CodexRemoteAdapter extends LlmAdapter {
|
|
|
88
90
|
jitterRatio: number;
|
|
89
91
|
};
|
|
90
92
|
private cleanupRuntime;
|
|
93
|
+
completeDshTurn(sessionId: string, messages: GenerateOptions['messages']): Promise<void>;
|
|
94
|
+
hasBridgeSession(sessionId: string): boolean;
|
|
91
95
|
disposeSession(sessionId: string): Promise<void>;
|
|
92
96
|
detachSession(sessionId: string): Promise<void>;
|
|
93
97
|
dispose(): Promise<void>;
|
|
@@ -97,7 +101,7 @@ export declare class CodexRemoteAdapter extends LlmAdapter {
|
|
|
97
101
|
/** One official registration owns both routes, preserving opaque replay ownership. */
|
|
98
102
|
export declare class RemoteAgentsAdapter extends LlmAdapter {
|
|
99
103
|
private adapters;
|
|
100
|
-
constructor(config: () => PluginConfig, makeRuntime: (agent: AgentDefinition) => RuntimeProvider, approve: InvocationApproval, nameFor: (agent: AgentDefinition) => string, readImage?: ImageReader, reportLoginFailure?: LoginFailureReporter, directory?: ModelDirectory, preflightDiagnostics?: PreflightDiagnostics, permissions?: PermissionSource);
|
|
104
|
+
constructor(config: () => PluginConfig, makeRuntime: (agent: AgentDefinition) => RuntimeProvider, approve: InvocationApproval, nameFor: (agent: AgentDefinition) => string, readImage?: ImageReader, reportLoginFailure?: LoginFailureReporter, directory?: ModelDirectory, preflightDiagnostics?: PreflightDiagnostics, permissions?: PermissionSource, bridgeStopped?: (sessionId: string, notice: string) => void);
|
|
101
105
|
private adapter;
|
|
102
106
|
providerInfo(provider: string): {
|
|
103
107
|
id: string;
|
|
@@ -108,8 +112,8 @@ export declare class RemoteAgentsAdapter extends LlmAdapter {
|
|
|
108
112
|
})[]>;
|
|
109
113
|
resolveModel(provider: string, model: string, signal?: AbortSignal): Promise<LlmResolvedModelInfo>;
|
|
110
114
|
prepareCall(provider: string, model: string, signal?: AbortSignal): Promise<{
|
|
111
|
-
model: LlmResolvedModelInfo;
|
|
112
115
|
stream: (options: GenerateOptions) => AsyncGenerator<StreamChunk, any, any>;
|
|
116
|
+
model: LlmResolvedModelInfo;
|
|
113
117
|
}>;
|
|
114
118
|
providerRetryPolicy(): {
|
|
115
119
|
mode: "normal";
|
|
@@ -120,6 +124,7 @@ export declare class RemoteAgentsAdapter extends LlmAdapter {
|
|
|
120
124
|
jitterRatio: number;
|
|
121
125
|
};
|
|
122
126
|
stream(options: GenerateOptions): AsyncGenerator<StreamChunk, any, any>;
|
|
127
|
+
completeDshTurn(sessionId: string, messages: GenerateOptions['messages']): Promise<void>;
|
|
123
128
|
disposeSession(sessionId: string): Promise<void>;
|
|
124
129
|
detachSession(sessionId: string): Promise<void>;
|
|
125
130
|
dispose(): Promise<void>;
|