@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 CHANGED
@@ -1,10 +1,20 @@
1
- # AI Workbench 远程助手 · @ai-workbench/remote-agent@0.8.28 · 插件 → 添加插件
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.29 或更新的 0.7.x,以及联网助手 0.4.9。主机来源保存于官方 storage.domain 的会话元数据表,每轮仅由连接组件检查一次主机目录,不检查本机占位目录。
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
- 断线会结束本轮;已派发的操作可能已部分执行,请先检查文件。恢复连接后可在原会话继续。旧进程清理和技术详情记录尽力而为,不阻止发起下一轮;如果上一轮仍占用原生线程,续接会明确失败并说明本轮输入未发送,不新建替代线程。自动定位、清理同一会话/线程的已断开旧进程仍需 S 提供公开能力,当前不会自动杀旧进程或冒称已恢复。原生本机会话恢复的兼容性及实际设备结果见本轮交付证据,未验证项不作为已通过。
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.8.24' },
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
+ }
@@ -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
  }
@@ -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
- mcp = await (options.ports?.mcp ?? openEmployeeMcp)({ closeDiagnostics, lease, maxBytes: maximum, killGraceMs: grace, runShell: shell, activeSignal: () => entry?.active?.signal, captureDetail: () => { const sink = entry?.active?.details; if (!sink)
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;
@@ -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;
@@ -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
- 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);
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>;