@ai-workbench/remote-agent 0.8.19 → 0.8.21

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,10 @@
1
- **`@ai-workbench/remote-agent@0.8.19` 发布形态包。包含X2主机目录上下文与审批位置修正;本版以aw.4对齐原生本机MCP过滤、保留远端关闭首因并汇总成功连接核对。继承0.8.18的执行时序、去重复SSH和失败先交还。通过官方插件 → 添加插件安装;固定包由编排发布,X不publish。工程/包证明与leo产品验收分别记录,未运行的门保持NOT_RUN。**
1
+ **`@ai-workbench/remote-agent@0.8.21` 发布形态包。包含X2主机目录上下文与审批位置修正;本版将本机HTTP扩展策略默认改为ignore,并按名称给出员工提示;stdio说明与协议细节仅在技术详情;继承aw.4原生MCP过滤、关闭首因保留与成功连接核对汇总。继承0.8.18的执行时序、去重复SSH和失败先交还。通过官方插件 → 添加插件安装;固定包由编排发布,X不publish。工程/包证明与leo产品验收分别记录,未运行的门保持NOT_RUN。**
2
2
 
3
3
  # 远程ACP插件
4
4
 
5
5
  一个插件提供 **Claude(远程主机)** 和 **Codex(远程主机)**,通过员工主机上的官方CLI运行。Codex保留单次模式;Claude固定会话模式。只用官方模型列表、插件管理和配置入口,不提供自制面板、问答或登录按钮。登录和原生工作区绑定唯一归连接插件L。
6
6
 
7
- 当前版本0.8.19,在0.8.18/aw.3基础上修复本机MCP过滤、关闭首因和连接核对降噪,需aw.4;包含X2主机目录上下文与审批位置修正;0.8.17的主机来源执行和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另验。
7
+ 当前版本0.8.21,继承0.8.19的本机MCP过滤、关闭首因和连接核对汇总,并将本机HTTP扩展策略默认改为ignore、按名称给出员工提示;stdio说明及协议细节仅在技术详情,新缺省需aw.5(显式策略仍兼容aw.4);包含X2主机目录上下文与审批位置修正;0.8.17的主机来源执行和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,7 @@
12
12
 
13
13
  ## 员工路径
14
14
 
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.19或Codex,产品验收以各版本leo实际结果为准。本地文件不自动上传,X不猜目录、不选首项。首次说明使用官方普通回复;先经官方会话提交、持久化并读回核对,才向L确认。持久记录不等于员工已看见,GUI另验;日志丢失后的未知claim由L保留,不宣称端到端恰好一次。
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.21;编排06:16另已回证0.8.19/aw.4的Sol本机你好及写文件通过,产品验收以各版本leo实际结果为准。本地文件不自动上传,X不猜目录、不选首项。首次说明使用官方普通回复;先经官方会话提交、持久化并读回核对,才向L确认。持久记录不等于员工已看见,GUI另验;日志丢失后的未知claim由L保留,不宣称端到端恰好一次。
16
16
 
17
17
  新主机位置允许当前SSH员工账号可访问的已选绝对目录、文件系统根目录、非Git或只读目录;不改写原projectRoot、不创建Git、不转本机同名文件夹。read/execute必须可核实,write=false不额外一刀切拒绝,实际写入服从主机系统权限。运行前必核来源UUID、连接代次、目录及链接入口dev/ino和权限;revision与内容增删导致的mtime/ctime变化不单独失效。此核对属于正确性条件,不可关闭。最后inspect到主机spawn之间仍有OS时序边界,不表示原子目录句柄租约。
18
18
 
@@ -49,7 +49,7 @@ Codex单次模式首版仅文本,不转接中途审批;图片或系统/开
49
49
  | oneShotSandbox | workspace-write | Codex单次可选read-only/workspace-write/danger-full-access |
50
50
  | oneShotApproval | never | 当前单次通道无中途交互审批;不代表自动批准 |
51
51
  | employeeShellKillGraceMs | 1000 | 0至2147483647的整数;取消后外层SSH清理宽限毫秒。取消结果仍未知,不将宽限作为员工端退出证明 |
52
- | employeeHttpMcpPolicy | reject | reject / ignore;新连接生效,默认拒绝原生会采用的本机HTTP MCP;ignore仅剔除会话投影后继续。不改员工配置;stdio按原生规则忽略并可见说明名称 |
52
+ | employeeHttpMcpPolicy | ignore | reject / ignore;新连接生效。默认ignore仅剔除原生会采用的本机HTTP扩展投影后继续;可选reject保留固定拒绝原因。不改员工配置。忽略的HTTP扩展按名称用员工用语提示,stdio名称及协议细节仅进技术详情 |
53
53
 
54
54
  原六项默认保留;D20新增employeeShellKillGraceMs供管理员调节自己的SSH清理。非法值拒绝,删除覆盖遵循官方配置继承。SSH可执行程序从当前PATH发现,不写死某一系统路径。原始错误码保留在官方失败代码/技术详情,原始stderr、凭据和URL不展开。
55
55
 
@@ -66,7 +66,7 @@ Codex单次模式首版仅文本,不转接中途审批;图片或系统/开
66
66
 
67
67
  ## 历史实现记录(0.8.1–0.8.16)
68
68
 
69
- 以下按版本保留当轮的设置数量、依赖、首红与验收状态;“本轮/当前”均指对应历史轮次。0.8.19当前配置以开头、管理员八项表及末节为准;这些历史工程记录不替代本版产品验收。
69
+ 以下按版本保留当轮的设置数量、依赖、首红与验收状态;“本轮/当前”均指对应历史轮次。0.8.21当前配置以开头、管理员八项表及末节为准;这些历史工程记录不替代本版产品验收。
70
70
 
71
71
  D19(0.8.1):相邻正文增量合成连续文本块,工作区首次说明合入真实回复,按官方持久记录核对后才回报“已记入可核对回复”。这不代表界面已经显示。
72
72
 
@@ -85,7 +85,7 @@ D21(0.8.5):两个品牌的官方模型菜单从 L 的 `modelCatalogTarget:
85
85
 
86
86
  每轮先核对目录代次和所选模型,再经 ACP 逐项配置模型及推理等级,收到完整配置确认后才发送输入。Claude 别名每轮须有精确的模型配置成功回执,并由同一员工主机、同一连接代际的目录唯一映射到具体型号。具体型号注明来自主机目录,会话未单独回报;旧恢复记录不作本轮证据。目录缺项、映射不唯一、回执不符或身份/代际变化时不发送输入。所选值、远端确认值及型号来源仅在恢复标识与技术详情保留,插件不向真实回复追加技术参数,确认配置不等于验证推理效果。连接/身份/目录变化清除旧确认;远端清理待定或结果未知时阻止派发,不自动清理重试或另开替代会话。Codex 单次模式当前不能确认此选择,会在发送前明确拒绝。新增设置 0;本轮工程证据、真实只读链路与产品未验项见 `docs/evidence/2026-10-22-X2-D21/`。
87
87
 
88
- 0.8.5统一包原依赖L `~0.7.13`;0.8.16原对齐L `~0.7.19`;当前0.8.19对齐L `~0.7.20`。D8模型目录双事件长期监听保留;同epoch的recovery提示/失败消息变化不触发执行状态失效,真实实例/epoch/phase/可信目标/登录变化仍失效。完整D9两品牌正文/状态/就绪门呈现按编排00:50允许的范围记下一版,本版不宣称已消费完成;L的connectionMessages.administratorAfterMs仍只由L管理;X本版新增employeeHttpMcpPolicy见末节。
88
+ 0.8.5统一包原依赖L `~0.7.13`;0.8.16原对齐L `~0.7.19`;当前0.8.21对齐L `~0.7.20`。D8模型目录双事件长期监听保留;同epoch的recovery提示/失败消息变化不触发执行状态失效,真实实例/epoch/phase/可信目标/登录变化仍失效。完整D9两品牌正文/状态/就绪门呈现按编排00:50允许的范围记下一版,本版不宣称已消费完成;L的connectionMessages.administratorAfterMs仍只由L管理;employeeHttpMcpPolicy自0.8.19新增,见后文;0.8.20无新增设置。
89
89
 
90
90
  D18(0.8.6):Codex 需要主机自有 adapter `2.2.2-aw.1` 的明确新建会话上下文政策,官方 CLI 仍为未改的0.160.1。系统/开发者要求仅在新建时生效;恢复时与初建摘要相同则正常恢复且省略新建专用字段,不声称恢复接口会更新指令。要求不同或清空时,在同一真实回复说明“会话要求已变化,已新建 Codex 会话继续处理,原会话仍保留。”,并新建远端承接公开对话,保留旧标识和查询记录;旧/非法初建摘要明确拒绝,不能猜测。
91
91
 
@@ -145,8 +145,30 @@ L接纳每轮真实输入时核对实际目录和N端点/代次。X只在该代
145
145
 
146
146
  ## 0.8.19 / aw.4 本机 MCP 与失败原因
147
147
 
148
- 按 Codex 0.160.1 原生层合并和 HTTP/header 能力过滤处理员工 MCP。stdio(例如 node_repl)不拒绝,显示“已忽略本机 stdio MCP: 名称”;不转发其 command、args、env 值,不修改员工文件。真正的 HTTP 条目使用具名设置 employeeHttpMcpPolicy:默认 reject 保留 AW_EMPLOYEE_EXECUTOR_MCP_UNSUPPORTED;ignore 只在会话投影剔除后继续。管理员可在现有远程助手参数页及官方配置文件入口编辑,范围、默认值、生效与后果均可见。此项在新连接冻结,修改不增加中止当前轮或旧会话的条件。stdio 的过滤来自固定原生协议,不是可调阈值。
148
+ 按 Codex 0.160.1 原生层合并和 HTTP/header 能力过滤处理员工 MCP。stdio(例如 node_repl)不拒绝;0.8.20起仅在技术详情显示“已忽略本机 stdio MCP: 名称”,不注入模型正文;不转发其 command、args、env 值,不修改员工文件。真正的 HTTP 条目使用具名设置 employeeHttpMcpPolicy:默认 reject 保留 AW_EMPLOYEE_EXECUTOR_MCP_UNSUPPORTED;ignore 只在会话投影剔除后继续。管理员可在现有远程助手参数页及官方配置文件入口编辑,范围、默认值、生效与后果均可见。此项在新连接冻结,修改不增加中止当前轮或旧会话的条件。stdio 的过滤来自固定原生协议,不是可调阈值。
149
149
 
150
150
  aw.4 仅在客户端声明 closeReason:1 后发送白名单 close.reason;旧端形状保持兼容。新版 X 接到明确远端 close 后释放员工 carrier,保留在途主机 session/new 回应,再由原本收尾释放 broker;非预期 EOF 仍按既有路径关闭。技术详情分别显示运行原因、首次观察和远端固定关闭原因;它们不因详情保存或清理失败而被替代。成功 connection-confirm 按阶段汇总次数和耗时,失败仍即时记录;实际确认操作不减少,无新增超时、重试、慢阈值或中止检查。
151
151
 
152
152
  03:44 现场的 stdio 首因仍属强推断,主机当时原码没有落盘;离线回放不能补造现场证据。aw.4 由编排审后交 S 安装,客户端固定包不由 X 发布;真实 leo Sol/Luna“你好”及写文件验收单独记录,不能用离线或包门代替。
153
+
154
+ ## 0.8.20 员工正文与剩余产品门
155
+
156
+ 本机扩展处理信息只进入既有技术详情,成功建会与失败关闭均不向员工正文注入stdio/MCP或扩展名;模型回复和固定失败原因保持原语义,不另加说明气泡。此项是界面呈现不变量,不新增设置、阈值、超时、重试或中止条件;主机aw.4字节不变。
157
+
158
+ 实际L0.7.22组合负例另确认:目录替换时L先发布具体Need,X原有取消等待会盖成ABORTED。现只保留同一主机观察器已记录的具体Need及员工下一步动作;用户主动取消和非公开错误保持原分支,不新增目录核验、撤销条件、请求或等待。
159
+
160
+ 编排10-24 06:16已确认L0.7.22/X0.8.19/N0.4.5/aw.4在leo本机目录Sol你好35秒完成,同会话自验-codex-1024h.md约30秒写入本机且内容正确。这是编排产品回证;本版仍须另验Luna本机你好及写文件、Sol默认工作区主机目录写文件。仅在N回执明确交还GUI后走SSH脚本,首红即停,系统弹窗不点。工程/包证据与各项实机结果见源码docs/evidence/2026-10-24-X-0820/RESULT.md。
161
+
162
+ ## 0.8.21 Claude执行核验的位置(不可关闭)
163
+
164
+ 主机目录在每轮接纳时由L核验登录、可信主机与物理目录;之后Claude/Codex每条消息、审批回复、检查点只核对内存中的连接、登录与工作区状态,沿原L连接/位置事件撤销。Claude本机每轮真实输入仍先由L接纳;租约打开时核验物理目录、L上下文、N prepare与S同代target,之后只核对内存里的连接、位置、网络和租约信号。旧租约被事件撤销后不能复活,不迁移跨代在途操作,不回退主机;实际本机工具入口保留原物理目录核验与永久撤销,命令执行前原dev/ino/UID核对不变。没有关闭开关、超时、重试或新增中止条件。管理页固定约束及每轮HOST_EXECUTION_VALIDATION_POLICY/CLAUDE_EMPLOYEE_VALIDATION_POLICY列明核验点与撤销来源。
165
+
166
+ Claude文本分片扩展按官方messageId汇总一条ACP_MODEL_METADATA,逐片脱敏快照及顺序保留;官方无messageId时按连续正文/思考段汇总,不捏造官方消息身份。响应、正常收尾及失败收尾都会提交未写扩展,正文/工具/审批转换原样。主机SSH偶发10–15秒受理卡顿、L单条写入耗时的底层根因仍未查明;去掉重复核验不代表这些问题已修复。leo两条路径的真实耗时与包交付证据另记,不以源码门冒充产品通过。
167
+
168
+ ## 0.8.21 / aw.5 owner HTTP 扩展策略
169
+
170
+ owner 10-24决定:employeeHttpMcpPolicy 默认由 reject 改为 ignore,管理界面仍保留 reject。客户端schema、缺省请求与aw.5主机缺省统一;管理员已显式保存的 reject 不被覆盖。新连接冻结策略,修改设置不新增中止当前轮或旧会话的条件。ignore仅清空本次环境配置投影,不改员工文件、不启动扩展、不新增网络路径;显式reject保留AW_EMPLOYEE_EXECUTOR_MCP_UNSUPPORTED。原aw.4固定字节不变,aw.5四输入经编排审后交S受保护安装。
171
+
172
+ 被忽略的HTTP扩展仅把安全名称转成员工提示,例如“你电脑上的『Jira』扩展本次不可用。”,不加入MCP/HTTP等协议词;原生能力过滤掉的HTTP扩展同样提示。同轮相同提示只出一次,名称用安全文本引用,控制字符名称不打印,使用员工通用说明。stdio始终只进技术详情;模型自身文本不滤词。完整协议原因和计数仍在技术详情,失败首因保持。此提示是执行事实不变量,没有另设开关、条数/长度阈值、超时、重试或新的中止检查。
173
+
174
+ 客户端统一由X2在D22与本增量合并main后出唯一0.8.21;本分支不产另一份同版包。0.8.20固定包及其真机记录不因本次默认值变化而重写。aw.5安装与0.8.21真实HTTP提示验收另记,源码/离线测试不冒产品通过。
package/dist/acp.d.ts CHANGED
@@ -16,9 +16,9 @@ export interface AcpHooks {
16
16
  onResponse?: (response: acp.PromptResponse) => void;
17
17
  approve?: (request: acp.RequestPermissionRequest, signal?: AbortSignal) => Promise<ApprovalOutcome>;
18
18
  onNeed?: (need: Need) => void;
19
+ onEmployeeNotice?: (notice: string) => void;
19
20
  onDetail?: (detail: Readonly<SanitizedObservation>) => void;
20
21
  broker?: BrokerTransport;
21
- onEmployeeInfo?: (summary: string) => void;
22
22
  employeeExecution?: (params: unknown, signal?: AbortSignal) => Promise<Record<string, unknown>>;
23
23
  }
24
24
  export interface EmployeeExecutionMeta {
@@ -78,6 +78,8 @@ export declare class AcpSession {
78
78
  private stage;
79
79
  get diagnostics(): readonly Readonly<SanitizedObservation>[];
80
80
  private detail;
81
+ private employeeNoticeNames;
82
+ private employeeNoticeUnshown;
81
83
  private employeeInfo;
82
84
  private need;
83
85
  private checkReporting;
package/dist/acp.js CHANGED
@@ -1,4 +1,4 @@
1
- import { employeeExecutionInfo, employeeMcpSummary } from "./employee-info.js";
1
+ import { employeeExecutionInfo, employeeMcpSummary, employeeHttpNotice } from "./employee-info.js";
2
2
  import { employeeAdapterCodes, employeeOpenCauseCodes } from "./employee-reasons.js";
3
3
  import { CloseDiagnostics, withFailureEvidence } from "./close-diagnostics.js";
4
4
  import * as acp from '@agentclientprotocol/sdk/unstable';
@@ -7,7 +7,7 @@ import { employeeFailure } from "./presentation.js";
7
7
  import { StageTiming } from "./stage-timing.js";
8
8
  import { isolateRpcIds } from "./rpc-namespace.js";
9
9
  import { recoveryCapabilities, supportsCodexContextPolicy } from "./recovery.js";
10
- import { sanitizeObservation } from "./conversation-details.js";
10
+ import { sanitizeObservation, sanitizeDisplayLabel } from "./conversation-details.js";
11
11
  import { ModelConfiguration, ModelConfigurationError } from "./model-configuration.js";
12
12
  /** ACP v1 with explicitly negotiated SDK 2.0 lifecycle/context extensions. */
13
13
  export class AcpSession {
@@ -135,14 +135,22 @@ export class AcpSession {
135
135
  this.close({ layer: 'acp', initiator: 'acp.reporting-failure', reason: 'CONVERSATION_DETAILS_FAILED', action: 'close' });
136
136
  }
137
137
  }
138
+ employeeNoticeNames = new Set();
139
+ employeeNoticeUnshown = false;
138
140
  employeeInfo(value) {
139
141
  const info = employeeExecutionInfo(value);
140
142
  if (!info)
141
143
  return;
142
144
  const summary = employeeMcpSummary(info);
143
145
  this.detail({ kind: 'session', severity: 'info', code: 'EMPLOYEE_MCP_INFORMATION', summary: summary || '本机MCP策略已应用:' + info.httpMcpPolicy, payload: info });
144
- if (summary)
145
- this.hooks.onEmployeeInfo?.(summary);
146
+ const notice = employeeHttpNotice({ ...info, ignoredHttp: info.ignoredHttp.filter(name => !this.employeeNoticeNames.has(name)), ignoredNativeHttp: info.ignoredNativeHttp.filter(name => !this.employeeNoticeNames.has(name)), ignoredHttpUnshown: this.employeeNoticeUnshown ? 0 : info.ignoredHttpUnshown, ignoredNativeHttpUnshown: this.employeeNoticeUnshown ? 0 : info.ignoredNativeHttpUnshown }, sanitizeDisplayLabel, !this.employeeNoticeUnshown);
147
+ if (notice) {
148
+ for (const name of [...(info.httpMcpPolicy === 'ignore' ? info.ignoredHttp : []), ...info.ignoredNativeHttp])
149
+ this.employeeNoticeNames.add(name);
150
+ if ((info.httpMcpPolicy === 'ignore' && info.ignoredHttpUnshown) || info.ignoredNativeHttpUnshown || [...(info.httpMcpPolicy === 'ignore' ? info.ignoredHttp : []), ...info.ignoredNativeHttp].some(name => sanitizeDisplayLabel(name) !== name || name.includes('[已脱敏:')))
151
+ this.employeeNoticeUnshown = true;
152
+ this.hooks.onEmployeeNotice?.(notice);
153
+ }
146
154
  }
147
155
  need(need, hooks = this.hooks) {
148
156
  this.detail({ kind: 'need', severity: 'action-required', code: need.code, summary: need.message, payload: { requires: need.requires } }, hooks);
@@ -385,6 +393,8 @@ export class AcpSession {
385
393
  if (this.busy || this.updating || this.selecting)
386
394
  throw new NeedError('TURN_BUSY', '上一轮或配置更新结束前不能更换会话钩子', '上一轮结束');
387
395
  this.hooks = hooks;
396
+ this.employeeNoticeNames.clear();
397
+ this.employeeNoticeUnshown = false;
388
398
  }
389
399
  get closed() { return this.connection.closed; }
390
400
  validateNewContext(text, requirePolicy = false) {
@@ -405,7 +415,7 @@ export class AcpSession {
405
415
  signal?.addEventListener('abort', abort, { once: true });
406
416
  try {
407
417
  const hello = await this.stage('acp-initialize', () => this.connection.agent.request(acp.methods.agent.initialize, {
408
- protocolVersion: acp.PROTOCOL_VERSION, clientInfo: { name: '@ai-workbench/remote-agent', version: '0.8.19' },
418
+ protocolVersion: acp.PROTOCOL_VERSION, clientInfo: { name: '@ai-workbench/remote-agent', version: '0.8.21' },
409
419
  clientCapabilities: { fs: { readTextFile: false, writeTextFile: false }, terminal: false, session: { notices: {} }, ...(this.employeeMeta ? { _meta: { awEmployeeExecution: { version: 1, closeReason: 1 } } } : {}) },
410
420
  }, { cancellationSignal: signal }));
411
421
  this.hooks.broker?.acknowledge(acp.methods.agent.initialize);
@@ -12,7 +12,7 @@ import { isDeepStrictEqual } from 'node:util';
12
12
  import { openEmployeeLease } from "./employee-endpoint.js";
13
13
  import { openEmployeeMcp } from "./employee-mcp.js";
14
14
  import { createEmployeeShell } from "./employee-shell.js";
15
- import { sameTarget, unwrap } from "./provider.js";
15
+ import { unwrap } from "./provider.js";
16
16
  import { sshIo } from "./broker.js";
17
17
  import { NeedError, shellQuote } from "./remote.js";
18
18
  import { awaitL } from "./provider.js";
@@ -101,13 +101,14 @@ export function createClaudeLocalProvider(options) {
101
101
  closeDiagnostics.bind(event => details.record(event));
102
102
  request.closeDiagnostics = closeDiagnostics;
103
103
  request.timing?.bind(observation => details.record(observation));
104
+ details.record({ kind: 'session', severity: 'info', code: 'CLAUDE_EMPLOYEE_VALIDATION_POLICY', summary: '本机执行核验不可关闭:每轮真实输入仍由L接纳核验;本机租约打开时核验物理目录、L上下文、N端点与S同代target。之后消息、审批与检查点只核对内存中的连接、位置、网络及租约信号,靠既有L连接/位置、N网络事件与执行通道关闭撤销;实际本机工具入口保留物理目录核验,命令执行前仍核对dev/ino,不回退主机。' });
104
105
  const trusted = async (ignoreCancellation = false) => {
105
106
  if (!ignoreCancellation)
106
107
  request.signal?.throwIfAborted();
107
108
  if (options.getConnect() !== api)
108
109
  throw failure('CONNECTION_LOST');
109
110
  const now = api.getConnectionState();
110
- if (now.phase !== 'connected' || now.instanceId !== connection.instanceId || now.epoch !== connection.epoch || !sameTarget(unwrap(await api.getSshTarget()), target))
111
+ if (now.phase !== 'connected' || now.instanceId !== connection.instanceId || now.epoch !== connection.epoch)
111
112
  throw failure('CONNECTION_LOST');
112
113
  };
113
114
  const openRequest = (row, cold = false) => ({ ...request, detailChannel: details, bindingId: item.bindingId, workspaceId: workspace.workspaceId, bindingRevision: state.revision, ...cold ? { recoveryCheckpointId: undefined } : {}, validateContext: async (cleanup = false) => { await trusted(cleanup); if (!cleanup)
package/dist/client.js CHANGED
@@ -1,12 +1,13 @@
1
1
  /* Official locale and provider-registration updates only; no custom UI or refresh. */
2
2
  window.__ModuleLoader__.load({id:'@ai-workbench/remote-agent',factory(require){
3
3
  const messages={"en":{"administratorNote":"Settings → Built-in plugins → 远程助手参数. Eight named settings with defaults, scope and effects.","providerName":"Codex (Remote Host)"},"zh":{"administratorNote":"管理员:设置 → 内置插件 → 远程助手参数。八项设置含默认值、范围、生效和后果说明。","providerName":"Codex(远程主机)"}};
4
- const settingsFields=[{"key":"mode","name":"Codex 运行方式","kind":"select","options":[{"value":"session","name":"会话模式"},{"value":"one-shot","name":"单次模式(当前版本不可运行)"}],"range":"session / one-shot","scope":"仅 Codex;Claude 始终使用会话模式。","when":"当前轮不变;更改后原会话下一轮会拒绝继续,需新建会话。","consequence":"会话模式保留连续对话;当前单次模式会明确拒绝运行,不会发给模型。不提供关闭会话身份核对的选项。","defaultValue":"session"},{"key":"acpMaxMessageBytes","name":"完整消息与工具输出容量(字节)","kind":"number","min":1,"max":9007199254740991,"range":"1~9007199254740991 的整数;0不能关闭上限","scope":"两位远程助手的完整会话消息;也用于模型目录、本机 MCP、员工执行 RPC 和工具输出容量。","when":"当轮快照不变;下一次独立目录查询读取新值;已有会话需新建后使用。","consequence":"调大可接收更大消息、占用更多内存;调小更早拒绝超限。超限明确停止,不截断、不自动重试;不能用0取消完整消息保护。","defaultValue":4194304},{"key":"brokerMaxFrameBytes","name":"主机会话回传单段容量(字节)","kind":"number","min":1,"max":9007199254740991,"range":"1~9007199254740991 的整数;0不能关闭上限","scope":"本插件接收主机会话单段输出的容量;不会替主机修改它自己的输出策略。","when":"当轮快照不变;更改后已有会话需新建。","consequence":"调大允许更大回传段、占用更多内存;调小可能拒绝主机合法输出。超限停止,不截断;不能关闭协议帧核验。","defaultValue":2097152},{"key":"brokerControlMaxBytes","name":"主机会话管理回复容量(字节)","kind":"number","min":1,"max":9007199254740991,"range":"1~9007199254740991 的整数;0不能关闭上限","scope":"主机会话管理命令的标准输出、标准错误各自容量;也用于本机执行桥的管理回复。","when":"当轮快照不变;更改后已有会话需新建。","consequence":"调大允许更长管理回复、占用更多内存;调小可能无法核实状态。超限明确停止,不能把不完整回复当成功;0不表示无限。","defaultValue":1048576},{"key":"oneShotSandbox","name":"Codex 单次任务权限模式","kind":"select","options":[{"value":"read-only","name":"只读"},{"value":"workspace-write","name":"工作区内可写"},{"value":"danger-full-access","name":"完全访问(高权限)"}],"range":"read-only / workspace-write / danger-full-access","scope":"仅 Codex 单次通道;不改变会话模式、本机工具或员工账号的系统权限。","when":"仅单次任务启动时适用;当前版本单次任务被拒绝,因此目前不生效。","consequence":"只读不允许单次任务写入;工作区内可写允许该目录写入;完全访问放宽单次沙箱。此项不是员工目录的权限开关,也不会解锁当前不可用的单次模式。","defaultValue":"workspace-write"},{"key":"oneShotApproval","name":"Codex 单次任务审批方式","kind":"fixed","readOnly":true,"range":"固定 never;不能切换","scope":"仅 Codex 单次通道;不改变会话模式的员工审批。","when":"仅单次启动参数;当前单次模式不可运行。","consequence":"单次通道没有中途交互审批能力,所以必须固定为无中途交互;不表示自动批准或允许所有操作。不能切换为自动回答旧审批。","defaultValue":"never"},{"key":"employeeShellKillGraceMs","name":"本机命令外层连接清理宽限(毫秒)","kind":"number","min":0,"max":2147483647,"range":"0~2147483647 的整数","scope":"仅 Claude 本机 Bash 取消后,本插件自己的外层 SSH 进程清理;不控制员工命令或 Codex 执行器。","when":"当轮快照不变;更改后已有会话需新建。","consequence":"大于0时先请求外层连接退出,再等待所设毫秒后强制清理;0表示不留宽限、立即强制清理外层连接,不是关闭取消。没有员工端退出确认时结果仍未知,不能据此自动重放或声称命令已停止。","defaultValue":1000},{"key":"employeeHttpMcpPolicy","name":"本机 HTTP MCP 策略","kind":"select","options":[{"value":"reject","name":"拒绝(默认)"},{"value":"ignore","name":"忽略后继续"}],"range":"reject / ignore","scope":"仅 Codex 员工本机执行器实际会采用的 HTTP MCP;stdio 始终依原生过滤忽略并说明名称。","when":"新连接建立时读取并冻结;修改不停止当轮或已有会话,需新建会话使用新值。","consequence":"默认 reject 会以明确代码拒绝这些 HTTP 条目;ignore 仅剔除会话投影后继续,不改员工配置、不启动 MCP、不新增网络路径。两种模式都显示实际忽略的安全名称。","defaultValue":"reject"}];
4
+ const settingsFields=[{"key":"mode","name":"Codex 运行方式","kind":"select","options":[{"value":"session","name":"会话模式"},{"value":"one-shot","name":"单次模式(当前版本不可运行)"}],"range":"session / one-shot","scope":"仅 Codex;Claude 始终使用会话模式。","when":"当前轮不变;更改后原会话下一轮会拒绝继续,需新建会话。","consequence":"会话模式保留连续对话;当前单次模式会明确拒绝运行,不会发给模型。不提供关闭会话身份核对的选项。","defaultValue":"session"},{"key":"acpMaxMessageBytes","name":"完整消息与工具输出容量(字节)","kind":"number","min":1,"max":9007199254740991,"range":"1~9007199254740991 的整数;0不能关闭上限","scope":"两位远程助手的完整会话消息;也用于模型目录、本机 MCP、员工执行 RPC 和工具输出容量。","when":"当轮快照不变;下一次独立目录查询读取新值;已有会话需新建后使用。","consequence":"调大可接收更大消息、占用更多内存;调小更早拒绝超限。超限明确停止,不截断、不自动重试;不能用0取消完整消息保护。","defaultValue":4194304},{"key":"brokerMaxFrameBytes","name":"主机会话回传单段容量(字节)","kind":"number","min":1,"max":9007199254740991,"range":"1~9007199254740991 的整数;0不能关闭上限","scope":"本插件接收主机会话单段输出的容量;不会替主机修改它自己的输出策略。","when":"当轮快照不变;更改后已有会话需新建。","consequence":"调大允许更大回传段、占用更多内存;调小可能拒绝主机合法输出。超限停止,不截断;不能关闭协议帧核验。","defaultValue":2097152},{"key":"brokerControlMaxBytes","name":"主机会话管理回复容量(字节)","kind":"number","min":1,"max":9007199254740991,"range":"1~9007199254740991 的整数;0不能关闭上限","scope":"主机会话管理命令的标准输出、标准错误各自容量;也用于本机执行桥的管理回复。","when":"当轮快照不变;更改后已有会话需新建。","consequence":"调大允许更长管理回复、占用更多内存;调小可能无法核实状态。超限明确停止,不能把不完整回复当成功;0不表示无限。","defaultValue":1048576},{"key":"oneShotSandbox","name":"Codex 单次任务权限模式","kind":"select","options":[{"value":"read-only","name":"只读"},{"value":"workspace-write","name":"工作区内可写"},{"value":"danger-full-access","name":"完全访问(高权限)"}],"range":"read-only / workspace-write / danger-full-access","scope":"仅 Codex 单次通道;不改变会话模式、本机工具或员工账号的系统权限。","when":"仅单次任务启动时适用;当前版本单次任务被拒绝,因此目前不生效。","consequence":"只读不允许单次任务写入;工作区内可写允许该目录写入;完全访问放宽单次沙箱。此项不是员工目录的权限开关,也不会解锁当前不可用的单次模式。","defaultValue":"workspace-write"},{"key":"oneShotApproval","name":"Codex 单次任务审批方式","kind":"fixed","readOnly":true,"range":"固定 never;不能切换","scope":"仅 Codex 单次通道;不改变会话模式的员工审批。","when":"仅单次启动参数;当前单次模式不可运行。","consequence":"单次通道没有中途交互审批能力,所以必须固定为无中途交互;不表示自动批准或允许所有操作。不能切换为自动回答旧审批。","defaultValue":"never"},{"key":"employeeShellKillGraceMs","name":"本机命令外层连接清理宽限(毫秒)","kind":"number","min":0,"max":2147483647,"range":"0~2147483647 的整数","scope":"仅 Claude 本机 Bash 取消后,本插件自己的外层 SSH 进程清理;不控制员工命令或 Codex 执行器。","when":"当轮快照不变;更改后已有会话需新建。","consequence":"大于0时先请求外层连接退出,再等待所设毫秒后强制清理;0表示不留宽限、立即强制清理外层连接,不是关闭取消。没有员工端退出确认时结果仍未知,不能据此自动重放或声称命令已停止。","defaultValue":1000},{"key":"employeeHttpMcpPolicy","name":"本机 HTTP MCP 策略","kind":"select","options":[{"value":"reject","name":"拒绝"},{"value":"ignore","name":"忽略后继续(默认)"}],"range":"reject / ignore","scope":"仅 Codex 员工本机执行器实际会采用的 HTTP MCP;stdio 始终依原生过滤忽略,名称仅在技术详情说明。","when":"新连接建立时读取并冻结;修改不停止当轮或已有会话,需新建会话使用新值。","consequence":"默认 ignore 仅剔除会话投影后继续;可选 reject 会以明确代码拒绝这些 HTTP 条目。ignore 不改员工配置、不启动 MCP、不新增网络路径。被忽略的 HTTP 扩展在正文按名称提示,技术细节仅在技术详情。","defaultValue":"ignore"}];
5
5
  /* Public settings.plugins.tab + shared ConfigForms. No private client services. */
6
6
  function mountRuntimeSettings(ctx,React,fields){
7
7
  const h=React.createElement;
8
8
  const invariants=[
9
9
  ['员工、主机和文件夹核对','始终开启,不可关闭','每次派发前核对当前连接、员工与工作区,避免操作到别人的文件或旧位置;核对失败即停止。'],
10
+ ['执行核验的位置与撤销','始终开启,不可关闭','主机目录在每轮接纳时核验身份、登录与物理目录;Claude本机租约在打开时核验物理目录、L上下文、N端点和S同代target;Codex本机同代通道也在打开时核验。后续消息、审批回复与检查点只核对内存状态,靠既有连接、位置、网络事件与执行通道关闭撤销,不重复SSH。每轮真实输入接纳和实际本机工具入口的物理目录核验仍保持。'],
10
11
  ['本轮输入与技术详情归属','始终开启,不可关闭','真实输入须先接纳,技术详情须确认保存;迟到结果仍属于原轮次,不能借下一轮的记录继续执行。'],
11
12
  ['模型选择确认','始终开启,不可关闭','主机须完整确认所选模型和推理等级,不能静默换模型继续。'],
12
13
  ['完整消息与精确整数','始终开启,不可关闭','容量不能用0关闭,超限明确停止、不截断;整数必须能精确表示,防止消息缺失或容量计算失真。'],
@@ -89,5 +89,7 @@ export interface EmployeeLease {
89
89
  signal: AbortSignal;
90
90
  validate(callerSignal?: AbortSignal): Promise<EmployeeEndpointTarget>;
91
91
  close(cause?: CloseCause): Promise<void>;
92
+ /** Existing physical fence, relocated to actual local tools rather than broker messages. */
93
+ validateDirectory(callerSignal?: AbortSignal): Promise<EmployeeEndpointTarget>;
92
94
  }
93
95
  export declare function openEmployeeLease(options: EmployeeLeaseOptions, initialSignal?: AbortSignal): Promise<EmployeeLease>;
@@ -68,19 +68,23 @@ export async function openEmployeeLease(options, initialSignal) {
68
68
  } };
69
69
  offs.push(api.onConnectionState(() => changed('lease.connection-change')), api.onWorkspaceLocationState(() => changed('lease.workspace-change')), network.onState(event => { if (event?.kind === 'lost' || network.getState().phase !== 'online')
70
70
  invalidate('EMPLOYEE_ENDPOINT_UNAVAILABLE', 'lease.network-loss'); }));
71
- async function validate(callerSignal) {
71
+ async function physicalDirectory(signal) {
72
+ current();
73
+ const now = await options.readWorkspace();
74
+ signal.throwIfAborted();
75
+ current();
76
+ const stat = await lstat(workspace.directory.path), canonical = await realpath(workspace.directory.path);
77
+ if (!isDeepStrictEqual(now, workspace) || !stat.isDirectory() || stat.isSymbolicLink() || canonical !== workspace.directory.path || stat.dev !== workspace.directory.device || stat.ino !== workspace.directory.inode)
78
+ throw fail();
79
+ }
80
+ // Remote endpoint discovery belongs to lease opening only.
81
+ async function openTarget(callerSignal) {
72
82
  if (diagnosticFailure)
73
83
  throw diagnosticFailure;
74
84
  callerSignal?.throwIfAborted();
75
85
  const signal = AbortSignal.any([lifetime.signal, ...callerSignal ? [callerSignal] : []]);
76
86
  try {
77
- current();
78
- const now = await options.readWorkspace();
79
- signal.throwIfAborted();
80
- current();
81
- const stat = await lstat(workspace.directory.path), canonical = await realpath(workspace.directory.path);
82
- if (!isDeepStrictEqual(now, workspace) || !stat.isDirectory() || stat.isSymbolicLink() || canonical !== workspace.directory.path || stat.dev !== workspace.directory.device || stat.ino !== workspace.directory.inode)
83
- throw fail();
87
+ await physicalDirectory(signal);
84
88
  const context = unwrap(await api.getLocalWorkspaceContext(request));
85
89
  signal.throwIfAborted();
86
90
  current();
@@ -123,9 +127,38 @@ export async function openEmployeeLease(options, initialSignal) {
123
127
  throw error instanceof NeedError || callerSignal?.aborted ? error : fail();
124
128
  }
125
129
  }
130
+ async function validate(callerSignal) {
131
+ if (diagnosticFailure)
132
+ throw diagnosticFailure;
133
+ callerSignal?.throwIfAborted();
134
+ try {
135
+ current();
136
+ return structuredClone(target);
137
+ }
138
+ catch (error) {
139
+ if (!callerSignal?.aborted)
140
+ invalidate(error instanceof NeedError ? error.code : 'EMPLOYEE_CONTEXT_CHANGED');
141
+ throw error instanceof NeedError || callerSignal?.aborted ? error : fail();
142
+ }
143
+ }
144
+ async function validateDirectory(callerSignal) {
145
+ if (diagnosticFailure)
146
+ throw diagnosticFailure;
147
+ callerSignal?.throwIfAborted();
148
+ const signal = AbortSignal.any([lifetime.signal, ...callerSignal ? [callerSignal] : []]);
149
+ try {
150
+ await physicalDirectory(signal);
151
+ return await validate(callerSignal);
152
+ }
153
+ catch (error) {
154
+ if (!callerSignal?.aborted)
155
+ invalidate(error instanceof NeedError ? error.code : 'EMPLOYEE_CONTEXT_CHANGED');
156
+ throw error instanceof NeedError || callerSignal?.aborted ? error : fail();
157
+ }
158
+ }
126
159
  try {
127
- await validate(initialSignal);
128
- return { target: structuredClone(target), request: Object.freeze({ ...request }), signal: lifetime.signal, validate, close: async (cause = { layer: 'endpoint', initiator: 'lease.close', reason: 'EMPLOYEE_EXECUTOR_CLOSED', action: 'close' }) => { options.closeDiagnostics?.record({ ...cause, changes: cause.changes ?? [{ field: 'lease.closed', before: closed, after: true }] }); if (!closed) {
160
+ await openTarget(initialSignal);
161
+ return { target: structuredClone(target), request: Object.freeze({ ...request }), signal: lifetime.signal, validate, validateDirectory, close: async (cause = { layer: 'endpoint', initiator: 'lease.close', reason: 'EMPLOYEE_EXECUTOR_CLOSED', action: 'close' }) => { options.closeDiagnostics?.record({ ...cause, changes: cause.changes ?? [{ field: 'lease.closed', before: closed, after: true }] }); if (!closed) {
129
162
  closed = true;
130
163
  lifetime.abort(fail('EMPLOYEE_EXECUTOR_CLOSED'));
131
164
  for (const off of offs)
@@ -11,3 +11,5 @@ export interface EmployeeExecutionInfo {
11
11
  }
12
12
  export declare function employeeExecutionInfo(value: unknown): EmployeeExecutionInfo | undefined;
13
13
  export declare function employeeMcpSummary(info: EmployeeExecutionInfo): string;
14
+ /** Employee-facing execution fact; protocol terms and stdio names remain in details. */
15
+ export declare function employeeHttpNotice(info: EmployeeExecutionInfo, sanitizeLabel: (name: string) => string, showUnshown: boolean): string;
@@ -26,3 +26,13 @@ export function employeeMcpSummary(info) {
26
26
  }
27
27
  return parts.join('。');
28
28
  }
29
+ /** Employee-facing execution fact; protocol terms and stdio names remain in details. */
30
+ export function employeeHttpNotice(info, sanitizeLabel, showUnshown) {
31
+ const rawNames = [...new Set([...(info.httpMcpPolicy === 'ignore' ? info.ignoredHttp : []), ...info.ignoredNativeHttp])];
32
+ const names = rawNames.filter(name => sanitizeLabel(name) === name && !name.includes('[已脱敏:'));
33
+ const unshown = showUnshown && (rawNames.length - names.length + (info.httpMcpPolicy === 'ignore' ? info.ignoredHttpUnshown : 0) + info.ignoredNativeHttpUnshown) > 0;
34
+ if (!names.length)
35
+ return unshown ? '你电脑上的一项本地扩展本次不可用,详情见技术详情。' : '';
36
+ const named = names.map(name => '『' + quoted([name]) + '』').join('、');
37
+ return '你电脑上的' + named + '扩展本次不可用。' + (unshown ? '另有本地扩展本次不可用,详情见技术详情。' : '');
38
+ }
@@ -88,7 +88,7 @@ export async function openEmployeeMcp(options) {
88
88
  if (!Number.isSafeInteger(options.maxBytes) || options.maxBytes < 1)
89
89
  throw new NeedError('SETTINGS_REQUIRED', '本机工具消息容量无效。', 'acpMaxMessageBytes');
90
90
  await access('/usr/bin/nc', constants.X_OK);
91
- const tools = createEmployeeTools({ closeDiagnostics: options.closeDiagnostics, cwd: options.lease.target.local.directory.path, shell: options.lease.target.endpoint.endpointShell, uid: options.lease.target.endpoint.remoteUid, maxBytes: options.maxBytes, killGraceMs: options.killGraceMs, detail: observe, runShell: options.runShell, validate: signal => options.lease.validate(signal).then(() => { }) });
91
+ const tools = createEmployeeTools({ closeDiagnostics: options.closeDiagnostics, cwd: options.lease.target.local.directory.path, shell: options.lease.target.endpoint.endpointShell, uid: options.lease.target.endpoint.remoteUid, maxBytes: options.maxBytes, killGraceMs: options.killGraceMs, detail: observe, runShell: options.runShell, validate: signal => options.lease.validateDirectory(signal).then(() => { }) });
92
92
  const root = await mkdtemp('/private/tmp/aw-x87-'), socketPath = root + '/mcp';
93
93
  try {
94
94
  const info = await lstat(root);
@@ -255,7 +255,7 @@ export function createEmployeeProvider(get, options, agent) {
255
255
  }
256
256
  const disposers = [];
257
257
  const execution = new EmployeeExecution({ closeDiagnostics, bindingId: admitted.bindingId, acpMaxMessageBytes: request.acpMaxMessageBytes, validate, transport: signal => createEmployeeSshTransport(ssh, signal) });
258
- const runtime = { ...host, closeDiagnostics, cwd: admitted.cwd, details, connection: { ...connection }, binding: { bindingId: admitted.bindingId, workspaceId: admitted.workspaceId, revision: state.revision }, saveCheckpoint: undefined, recoveredSession: undefined, employeeExecutionMeta: { version: 1, bindingId: admitted.bindingId, workspaceId: admitted.workspaceId, cwd: admitted.cwd, acpMaxMessageBytes: request.acpMaxMessageBytes, httpMcpPolicy: request.employeeHttpMcpPolicy ?? 'reject' }, employeePresentation: CODEX_EMPLOYEE_SCOPE_NOTICE };
258
+ const runtime = { ...host, closeDiagnostics, cwd: admitted.cwd, details, connection: { ...connection }, binding: { bindingId: admitted.bindingId, workspaceId: admitted.workspaceId, revision: state.revision }, saveCheckpoint: undefined, recoveredSession: undefined, employeeExecutionMeta: { version: 1, bindingId: admitted.bindingId, workspaceId: admitted.workspaceId, cwd: admitted.cwd, acpMaxMessageBytes: request.acpMaxMessageBytes, httpMcpPolicy: request.employeeHttpMcpPolicy ?? 'ignore' }, employeePresentation: CODEX_EMPLOYEE_SCOPE_NOTICE };
259
259
  runtime.employeeRemoteCloseReason = () => execution.remoteClose?.reason;
260
260
  entry = { scope, runtime, current: input, details, execution, network, ended: false, dispose: (reason, cause = { layer: 'runtime', initiator: 'provider.dispose', reason: reason?.code ?? 'EMPLOYEE_EXEC_CLOSED', action: 'dispose' }) => {
261
261
  closeDiagnostics.record({ ...cause, changes: cause.changes ?? [{ field: 'provider.ended', before: entry.ended, after: true }] });
@@ -96,8 +96,12 @@ export async function hostWorkspaceGuard(get, api, connection, target, request,
96
96
  return value;
97
97
  }
98
98
  catch (error) {
99
- if (!request.signal?.aborted)
99
+ if (!request.signal?.aborted) {
100
+ const cause = revoked.signal.reason;
100
101
  revoke(error);
102
+ if (cause instanceof NeedError)
103
+ throw cause;
104
+ }
101
105
  throw error;
102
106
  }
103
107
  }
package/dist/index.d.ts CHANGED
@@ -21,7 +21,7 @@ export declare const DEFAULT_CONFIG: Readonly<{
21
21
  readonly oneShotSandbox: "workspace-write";
22
22
  readonly oneShotApproval: "never";
23
23
  readonly employeeShellKillGraceMs: 1000;
24
- readonly employeeHttpMcpPolicy: "reject";
24
+ readonly employeeHttpMcpPolicy: "ignore";
25
25
  }>;
26
26
  export declare const Config: Schema<Schemastery.ObjectS<NoInfer<{
27
27
  employeeHttpMcpPolicy: Schema<"reject" | "ignore", "reject" | "ignore", "volatile-defined">;
package/dist/index.js CHANGED
@@ -45,9 +45,9 @@ export const settingsNamespace = 'ai-workbench-codex';
45
45
  // L can be installed/enabled after X; keeping X mounted lets it explain the prerequisite.
46
46
  export const inject = ['llm'];
47
47
  /** Owner-approved defaults; administrators may override through the existing settings. */
48
- export const DEFAULT_CONFIG = Object.freeze({ mode: 'session', acpMaxMessageBytes: 4194304, brokerMaxFrameBytes: 2097152, brokerControlMaxBytes: 1048576, oneShotSandbox: 'workspace-write', oneShotApproval: 'never', employeeShellKillGraceMs: 1000, employeeHttpMcpPolicy: 'reject' });
48
+ export const DEFAULT_CONFIG = Object.freeze({ mode: 'session', acpMaxMessageBytes: 4194304, brokerMaxFrameBytes: 2097152, brokerControlMaxBytes: 1048576, oneShotSandbox: 'workspace-write', oneShotApproval: 'never', employeeShellKillGraceMs: 1000, employeeHttpMcpPolicy: 'ignore' });
49
49
  export const Config = Schema.object({
50
- employeeHttpMcpPolicy: Schema.union([Schema.const('reject').i18n({ zh: '拒绝本机 HTTP MCP(默认)', en: '拒绝本机 HTTP MCP(默认)' }), Schema.const('ignore').i18n({ zh: '忽略本机 HTTP MCP 后继续', en: '忽略本机 HTTP MCP 后继续' })]).i18n({ zh: '本机 HTTP MCP 策略;默认拒绝;忽略仅剔除会话投影,不改员工配置。新连接生效,stdio 始终按原生规则忽略并说明名称', en: '本机 HTTP MCP 策略;默认拒绝;忽略仅剔除会话投影,不改员工配置。新连接生效,stdio 始终按原生规则忽略并说明名称' }).default(DEFAULT_CONFIG.employeeHttpMcpPolicy).volatile(),
50
+ employeeHttpMcpPolicy: Schema.union([Schema.const('reject').i18n({ zh: '拒绝本机 HTTP MCP', en: '拒绝本机 HTTP MCP' }), Schema.const('ignore').i18n({ zh: '忽略本机 HTTP MCP 后继续(默认)', en: '忽略本机 HTTP MCP 后继续(默认)' })]).i18n({ zh: '本机 HTTP MCP 策略;默认忽略;忽略仅剔除会话投影,不改员工配置。新连接生效,stdio 始终按原生规则忽略,名称仅在技术详情说明;HTTP 忽略在正文按扩展名称提示', en: '本机 HTTP MCP 策略;默认忽略;忽略仅剔除会话投影,不改员工配置。新连接生效,stdio 始终按原生规则忽略,名称仅在技术详情说明;HTTP 忽略在正文按扩展名称提示' }).default(DEFAULT_CONFIG.employeeHttpMcpPolicy).volatile(),
51
51
  mode: Schema.union([Schema.const('session').i18n({ en: 'Session', zh: '会话模式' }), Schema.const('one-shot').i18n({ en: 'One-shot', zh: '单次模式' })]).i18n({ en: 'Run mode; start a new session after switching modes', zh: 'Codex运行模式;Claude固定会话模式;切换后请新建会话' }).default(DEFAULT_CONFIG.mode).volatile(),
52
52
  acpMaxMessageBytes: Schema.number().min(1).step(1).i18n({ en: 'Session message limit; default 4 MiB, adjustable by administrators', zh: '会话消息容量;默认4 MiB,管理员可调整' }).default(DEFAULT_CONFIG.acpMaxMessageBytes).volatile(),
53
53
  brokerMaxFrameBytes: Schema.number().min(1).step(1).i18n({ en: 'Host output chunk limit; default 2 MiB, adjustable by administrators', zh: '主机回传单段容量;默认2 MiB,管理员可调整' }).default(DEFAULT_CONFIG.brokerMaxFrameBytes).volatile(),
@@ -536,7 +536,7 @@ export class CodexRemoteAdapter extends LlmAdapter {
536
536
  let done = false;
537
537
  let failure;
538
538
  const push = (chunks) => { queue.push(...chunks); wake?.(); wake = undefined; };
539
- const hooks = { onEmployeeInfo: summary => push(mapper.text(summary + '\n\n')), closeDiagnostics: runtime.closeDiagnostics, broker: runtime.broker, employeeExecution: runtime.employeeExecution, timing, approve: async (permission, approvalSignal) => {
539
+ const hooks = { closeDiagnostics: runtime.closeDiagnostics, broker: runtime.broker, employeeExecution: runtime.employeeExecution, timing, approve: async (permission, approvalSignal) => {
540
540
  const execution = runtime.execution;
541
541
  const toolName = permission.toolCall.name ?? permission.toolCall._meta?.claudeCode?.toolName;
542
542
  if (execution) {
@@ -551,7 +551,7 @@ export class CodexRemoteAdapter extends LlmAdapter {
551
551
  if (execution && answer === 'allowed-once')
552
552
  await execution.validate(approvalSignal);
553
553
  return answer;
554
- }, onUpdate: notification => { try {
554
+ }, onEmployeeNotice: notice => push(mapper.text(notice + '\n\n')), onUpdate: notification => { try {
555
555
  push(mapper.acp(notification.update));
556
556
  }
557
557
  catch (error) {
@@ -669,6 +669,7 @@ export class CodexRemoteAdapter extends LlmAdapter {
669
669
  yield chunk;
670
670
  }
671
671
  }
672
+ mapper.flushMetadata();
672
673
  recordInformation();
673
674
  details.endTurn();
674
675
  await timing.run('details-flush', () => details.flush());
@@ -714,6 +715,12 @@ export class CodexRemoteAdapter extends LlmAdapter {
714
715
  if (live && session && session.promptDispatchCount > promptBaseline)
715
716
  live.failure = { cause: error };
716
717
  const reporting = [];
718
+ try {
719
+ activeMapper?.flushMetadata();
720
+ }
721
+ catch (failure) {
722
+ reporting.push(failure);
723
+ }
717
724
  const reportFailure = async () => {
718
725
  if (request?.recordEarlyFailure) {
719
726
  const record = request.recordEarlyFailure;
package/dist/mapping.d.ts CHANGED
@@ -26,6 +26,9 @@ export declare class StreamMapper {
26
26
  private seen;
27
27
  private baseline;
28
28
  private readonly observations;
29
+ private readonly modelMetadata;
30
+ private anonymousMetadata;
31
+ private lastMetadataType;
29
32
  private readonly onDiagnostic;
30
33
  /** Omitted argument is exclusively for a known new session; pass null on unknown resume. */
31
34
  constructor(previousUsage?: UsageSnapshot | null, onDiagnostic?: (observation: Readonly<SanitizedObservation>) => void);
@@ -33,6 +36,8 @@ export declare class StreamMapper {
33
36
  get diagnostics(): readonly Readonly<SanitizedObservation>[];
34
37
  private diagnostic;
35
38
  private usageWarning;
39
+ /** One durable observation per official message; keep ordered, sanitized fragment snapshots. */
40
+ flushMetadata(): void;
36
41
  text(text: string, reasoning?: boolean): StreamChunk[];
37
42
  /** Close the current text or reasoning block once, before the stream settles. */
38
43
  flush(): StreamChunk[];
package/dist/mapping.js CHANGED
@@ -9,6 +9,9 @@ export class StreamMapper {
9
9
  seen = new Map();
10
10
  baseline;
11
11
  observations = [];
12
+ modelMetadata = new Map();
13
+ anonymousMetadata = 0;
14
+ lastMetadataType;
12
15
  onDiagnostic;
13
16
  /** Omitted argument is exclusively for a known new session; pass null on unknown resume. */
14
17
  constructor(previousUsage = { version: 1, kind: 'zero' }, onDiagnostic) {
@@ -30,6 +33,15 @@ export class StreamMapper {
30
33
  usageWarning(summary) {
31
34
  return this.diagnostic({ kind: 'usage', severity: 'warning', code: 'USAGE_UNAVAILABLE', summary });
32
35
  }
36
+ /** One durable observation per official message; keep ordered, sanitized fragment snapshots. */
37
+ flushMetadata() {
38
+ for (const [key, payload] of this.modelMetadata) {
39
+ // A throwing sink may already have enqueued its write. Never replay that message.
40
+ this.modelMetadata.delete(key);
41
+ this.diagnostic({ kind: 'extension', severity: 'info', code: 'ACP_MODEL_METADATA', summary: '远端模型消息扩展(按消息汇总)', method: 'session/update', payload });
42
+ }
43
+ this.lastMetadataType = undefined;
44
+ }
33
45
  text(text, reasoning = false) {
34
46
  if (!text)
35
47
  return [];
@@ -55,15 +67,31 @@ export class StreamMapper {
55
67
  acp(update) {
56
68
  if (update.sessionUpdate === 'agent_message_chunk' || update.sessionUpdate === 'agent_thought_chunk') {
57
69
  if (update.content.type === 'text' && typeof update.content.text === 'string') {
58
- // The whole update preserves every extension layer without inventing a display mapping.
70
+ const messageId = typeof update.messageId === 'string' ? update.messageId : undefined;
71
+ if (messageId !== undefined)
72
+ this.lastMetadataType = undefined;
73
+ else {
74
+ if (this.lastMetadataType !== update.sessionUpdate)
75
+ this.anonymousMetadata++;
76
+ this.lastMetadataType = update.sessionUpdate;
77
+ }
78
+ // Capture each extension layer now, before the caller can mutate it. Text mapping is unchanged.
59
79
  if (Object.keys(update).some(key => key !== 'sessionUpdate' && key !== 'content')
60
80
  || Object.keys(update.content).some(key => key !== 'type' && key !== 'text')) {
61
- this.diagnostic({ kind: 'extension', severity: 'info', code: 'ACP_MODEL_METADATA', summary: '远端模型消息扩展', method: 'session/update', payload: update });
81
+ const key = JSON.stringify(messageId !== undefined ? ['message', messageId] : ['segment', this.anonymousMetadata]);
82
+ let pending = this.modelMetadata.get(key);
83
+ if (!pending) {
84
+ pending = { sessionUpdate: update.sessionUpdate, ...(messageId !== undefined ? { messageId } : {}), fragments: [] };
85
+ this.modelMetadata.set(key, pending);
86
+ }
87
+ pending.fragments.push(sanitizeObservation({ kind: 'extension', severity: 'info', code: 'ACP_MODEL_METADATA', summary: '远端模型消息扩展', payload: update }).payload);
62
88
  }
63
89
  return this.text(update.content.text, update.sessionUpdate === 'agent_thought_chunk');
64
90
  }
91
+ this.lastMetadataType = undefined;
65
92
  return this.diagnostic({ kind: 'extension', severity: 'warning', code: 'ACP_CONTENT_UNSUPPORTED', summary: '远端非文本内容,需要对应的公开显示映射', method: 'session/update', payload: update });
66
93
  }
94
+ this.lastMetadataType = undefined;
67
95
  let kind = 'session';
68
96
  let code = 'ACP_SESSION_UPDATE';
69
97
  let summary = '远端会话事件';
@@ -111,6 +139,7 @@ export class StreamMapper {
111
139
  }
112
140
  /** ACP usage and the fixed Claude quota both accumulate across the session, not per turn. */
113
141
  acpResponse(response) {
142
+ this.flushMetadata();
114
143
  this.diagnostic({ kind: 'usage', severity: 'info', code: 'ACP_PROMPT_RESPONSE', summary: '远端响应与真实用量', method: 'session/prompt', payload: response });
115
144
  const quota = record(response._meta?.quota);
116
145
  const tokens = record(quota?.token_count);
package/dist/provider.js CHANGED
@@ -305,6 +305,8 @@ export function connectProvider(get, options = {}) {
305
305
  request.timing.bind(detail => details.record(detail));
306
306
  await stable();
307
307
  host?.assertCurrent();
308
+ if (host)
309
+ details?.record({ kind: 'session', severity: 'info', code: 'HOST_EXECUTION_VALIDATION_POLICY', summary: '主机目录核验不可关闭:每轮接纳时由L核验主机身份、登录与物理目录;之后消息、审批回复与检查点只核对内存中的连接、登录与位置,靠已订阅的L连接和位置事件撤销,不重复SSH。' });
308
310
  let workspaceNotice = undefined;
309
311
  const notice = !host && 'notice' in resolved ? resolved.notice : undefined;
310
312
  if (notice) {
@@ -319,8 +321,6 @@ export function connectProvider(get, options = {}) {
319
321
  const validateContext = async (ignoreCancellation = false) => {
320
322
  const check = () => waitForLogin(api, agent.loginProvider, connection, get, ignoreCancellation ? undefined : request.signal);
321
323
  await check();
322
- if (host && !ignoreCancellation)
323
- await host.resolve();
324
324
  if (observedBinding) {
325
325
  const current = api.getWorkspaceBindingState();
326
326
  if (current.instanceId !== observedBinding.instanceId || !current.items.some(item => item.bindingId === resolved.bindingId && item.workspaceId === resolved.workspaceId && item.status === 'ready'))
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ai-workbench/remote-agent",
3
- "version": "0.8.19",
3
+ "version": "0.8.21",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",