@ai-workbench/remote-agent 0.8.17 → 0.8.19

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.17` 发布形态包。Claude、Codex 接入 L0.7.20 已选择的主机文件夹;成功核验来源和目录后才打开技术详情与派发,每次派发前重新核验。L 的具体原因和下一步动作直接给员工。包含0.8.16首次关闭、运行、诊断保存和清理结果留痕;历史原始撤销字段与61秒慢查询根因仍未查明。本版 leo 主机位置/真实员工链路尚未复验。通过官方插件 → 添加插件安装;固定包由编排发布,X2 不 publish。**
1
+ **`@ai-workbench/remote-agent@0.8.19` 发布形态包。包含X2主机目录上下文与审批位置修正;本版以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.17,由X2按编排裁定基于0.8.16交付主机来源执行和L具体Need透传;包名采用`@ai-workbench/remote-agent`以涵盖两个品牌;本轮按编排放行准备发布形态包,供 leo 真机自验;X 不执行实际发布,由编排统一发布。开发fixture逐字节保留L0.7.20固定包,公开类型与SSH导出从该包消费。运行时仍要求L的`getContractInfo()`同时返回API0.7.0、implemented、native-local-v1;旧L包、draft.3或只有版本号的不完整实现会明确报缺项。依赖范围为`~0.7.20`,L由官方插件管理单独安装,不打进X。组件验证不代表真实开通、模型或leo E通过;HanaMesh另验。
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另验。
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.17或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.19或Codex,产品验收以各版本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
 
@@ -38,7 +38,7 @@ Codex单次模式首版仅文本,不转接中途审批;图片或系统/开
38
38
 
39
39
  ## 管理员配置
40
40
 
41
- 在官方 **设置 → 内置插件 → 远程助手参数** 查看和修改本插件全部七项参数。页面走官方ConfigForms与settings.plugins.tab,不新增连接设置节、不改DSH核心。每项列明默认值、范围、生效和调整后果;固定never及正确性约束明确列为不可关闭。当前profile不可写、服务未就绪或远程Web不支持持久写入时禁写并说明原因。保存携带编辑起点revision,冲突保留草稿、不自动重试;恢复继承值发unset,不写死默认。原生模型Edit卡未增加表单;“打开配置文件”仍可用,但不作为参数可见性的替代。
41
+ 在官方 **设置 → 内置插件 → 远程助手参数** 查看和修改本插件全部八项参数。页面走官方ConfigForms与settings.plugins.tab,不新增连接设置节、不改DSH核心。每项列明默认值、范围、生效和调整后果;固定never及正确性约束明确列为不可关闭。当前profile不可写、服务未就绪或远程Web不支持持久写入时禁写并说明原因。保存携带编辑起点revision,冲突保留草稿、不自动重试;恢复继承值发unset,不写死默认。原生模型Edit卡未增加表单;“打开配置文件”仍可用,但不作为参数可见性的替代。
42
42
 
43
43
  | 设置 | 默认 | 范围与用途 |
44
44
  |---|---|---|
@@ -49,10 +49,11 @@ 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
53
 
53
54
  原六项默认保留;D20新增employeeShellKillGraceMs供管理员调节自己的SSH清理。非法值拒绝,删除覆盖遵循官方配置继承。SSH可执行程序从当前PATH发现,不写死某一系统路径。原始错误码保留在官方失败代码/技术详情,原始stderr、凭据和URL不展开。
54
55
 
55
- 仅新增中文文案和验证;保留已有英文名称及既有locale机制。提供方名称不附状态后缀,由原注册replace更新,不自造刷新或客户端品牌分支。
56
+ 提供方的中文文案保留已有英文名称及既有locale机制。提供方名称不附状态后缀,由原注册replace更新,不自造刷新或客户端品牌分支。
56
57
 
57
58
  ## 安装产物
58
59
 
@@ -60,9 +61,13 @@ Codex单次模式首版仅文本,不转接中途审批;图片或系统/开
60
61
 
61
62
  ## 验证范围
62
63
 
63
- 当前准备结果与限制记入源码仓`docs/evidence/2026-10-21-X-npm-prep/`(不进npm包)。先前真实emp-a协议诊断及固定三件套GUI是不同轮次,不能代替本次新包安装门。L完整绑定/登录、真实模型、物理断网、HanaMesh及leo E以实际已执行证据为准。主机命令安装来自S,X不修改S仓/安装脚本或登录状态。
64
+ 早期npm准备结果与限制记入源码仓`docs/evidence/2026-10-21-X-npm-prep/`(不进npm包)。0.8.19组合工程结果见`docs/evidence/2026-10-24-X-aw4-0819/RESULT.md`。先前真实emp-a协议诊断及固定三件套GUI是不同轮次,不能代替本次新包安装门。L完整绑定/登录、真实模型、物理断网、HanaMesh及leo E以实际已执行证据为准。主机命令安装来自S,X不修改S仓/安装脚本或登录状态。
64
65
 
65
66
 
67
+ ## 历史实现记录(0.8.1–0.8.16)
68
+
69
+ 以下按版本保留当轮的设置数量、依赖、首红与验收状态;“本轮/当前”均指对应历史轮次。0.8.19当前配置以开头、管理员八项表及末节为准;这些历史工程记录不替代本版产品验收。
70
+
66
71
  D19(0.8.1):相邻正文增量合成连续文本块,工作区首次说明合入真实回复,按官方持久记录核对后才回报“已记入可核对回复”。这不代表界面已经显示。
67
72
 
68
73
  技术详情由 L 0.7.11 的唯一“远程主机连接”设置节展示,正文只放模型文本。命令目录保留完整描述;未知事件和工具输入输出保留字段、JSON类型及处理结果,未获准原文明确脱敏,数字用量可核对。没有 `remoteConversationDetails:1` 时明确提示“技术详情暂不可用”,本轮不发送模型请求。X 在官方当前 profile 的 `ai-workbench-remote-agent/diagnostics/` 保存私有诊断日志(目录700、文件600、落盘同步);写入/归属/顺序/容量失败中止本轮,不自动重发、换代重投、过期或淘汰。结果未知时只读核对原事件完整回执,未找到仍保持未知。X本轮无新增设置/阈值;L的单事件容量和分页设置在L设置节可见。
@@ -74,13 +79,13 @@ L安全规范的officialReason原串完整保留;旧/不支持格式明确记O
74
79
 
75
80
  0.8.3 会话恢复:正常关闭客户端保留原broker;重开仅在私有clean检查点、完整ACP消息边界、原broker UUID及官方最新持久回复一致时接续。否则记录原因、独占并确认结束旧会话后新建,正文说明一次;旧审批不代答,按官方当前步骤保留本批全部输入,不重发历史未确认输入。S0.4.10 的实际冻结controlMaxBytes用于分块,心跳/等待严格消费主机公开策略与remainingLeaseMs;没有新增X超时或重试设置。编排00:05决定:0.4.10以前的x/a仅为测试会话,X不接入、不代答也不结束它们,直接新建r会话并说明;旧进程由编排安装S0.4.10时一次性整理。重开续接只对S0.4.10新broker承诺。
76
81
 
77
- D22阶段耗时进入技术详情;D23文件审批用中文动作,原工具名和会话ID进入技术详情。恢复记录沿官方profile的ai-workbench-remote-agent/recovery,目录700/文件600/fsync,无过期或淘汰。产品设置仍为上列六项,无新增默认/阈值。含真实S进程的工程测试按文件串行运行,避免多个持久化/编译测试同时争用磁盘;测试观察期限和所有断言不变,工程证据不等于leo验收。
82
+ D22阶段耗时进入技术详情;D23文件审批用中文动作,原工具名和会话ID进入技术详情。恢复记录沿官方profile的ai-workbench-remote-agent/recovery,目录700/文件600/fsync,无过期或淘汰。当时产品设置为六项,无新增默认/阈值。含真实S进程的工程测试按文件串行运行,避免多个持久化/编译测试同时争用磁盘;测试观察期限和所有断言不变,工程证据不等于leo验收。
78
83
 
79
84
  D21(0.8.5):两个品牌的官方模型菜单从 L 的 `modelCatalogTarget:1` / `getModelCatalogTarget` 取得当前可信员工目标,再只读调用 S 的 `aw-model-catalog --provider codex|claude`。目录按 host-user 作用域展示完整模型与逐模型推理等级,保留远端标识原值;失败显示“无法获取模型列表:原因”,不登录、不创建项目、不返回替代模型。L 必须宣告该正式能力,S 必须安装保护入口;这两项未就绪时不能据组件测试声称菜单可用。
80
85
 
81
86
  每轮先核对目录代次和所选模型,再经 ACP 逐项配置模型及推理等级,收到完整配置确认后才发送输入。Claude 别名每轮须有精确的模型配置成功回执,并由同一员工主机、同一连接代际的目录唯一映射到具体型号。具体型号注明来自主机目录,会话未单独回报;旧恢复记录不作本轮证据。目录缺项、映射不唯一、回执不符或身份/代际变化时不发送输入。所选值、远端确认值及型号来源仅在恢复标识与技术详情保留,插件不向真实回复追加技术参数,确认配置不等于验证推理效果。连接/身份/目录变化清除旧确认;远端清理待定或结果未知时阻止派发,不自动清理重试或另开替代会话。Codex 单次模式当前不能确认此选择,会在发送前明确拒绝。新增设置 0;本轮工程证据、真实只读链路与产品未验项见 `docs/evidence/2026-10-22-X2-D21/`。
82
87
 
83
- 0.8.5统一包原依赖L `~0.7.13`;0.8.16原对齐L `~0.7.19`;当前0.8.17对齐L `~0.7.20`。D8模型目录双事件长期监听保留;同epoch的recovery提示/失败消息变化不触发执行状态失效,真实实例/epoch/phase/可信目标/登录变化仍失效。完整D9两品牌正文/状态/就绪门呈现按编排00:50允许的范围记下一版,本版不宣称已消费完成;L的connectionMessages.administratorAfterMs仍只由L管理,X新增设置0。
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见末节。
84
89
 
85
90
  D18(0.8.6):Codex 需要主机自有 adapter `2.2.2-aw.1` 的明确新建会话上下文政策,官方 CLI 仍为未改的0.160.1。系统/开发者要求仅在新建时生效;恢复时与初建摘要相同则正常恢复且省略新建专用字段,不声称恢复接口会更新指令。要求不同或清空时,在同一真实回复说明“会话要求已变化,已新建 Codex 会话继续处理,原会话仍保留。”,并新建远端承接公开对话,保留旧标识和查询记录;旧/非法初建摘要明确拒绝,不能猜测。
86
91
 
@@ -94,7 +99,7 @@ Read/Write/Edit/Glob/Grep由员工DSH同UID后端执行;Bash经L严格SSH到S
94
99
 
95
100
  本机执行在每个DSH会话首次用一句“文件和命令在你的电脑上执行”说明;既有0.8.14说明也计为已显示。完整限制每轮记录在技术详情,遇到不支持的操作再明确提示:子代理和harness内部直接工具调用尚未证明可在员工端执行,可能仍在主机;已识别的原生未改道工具/Agent审批拒绝。PDF等二进制读取及后台命令明确不支持,不静默主机代跑。六类组件/MCP/配置转交证明不等于真实Claude原生alias执行或真实员工链路通过。实际Git依赖员工环境中已有Git,缺失会返回实际命令失败,不在主机替代,也不自动安装。
96
101
 
97
- 本地目前仅支持macOS(arm64/x64);Grep固定携带微软@vscode/ripgrep-universal1.18.0的两架构原字节并核SHA,无员工PATH搜索或安装下载。主机需S0.4.11,N可选peer~0.4.4(缺失明确报能力缺项),L当前~0.7.18,本地接纳与D19工程路径已接,真实员工端门仍开放。无DSH/CLI修改、额外设置节或npm发布。
102
+ 本地目前仅支持macOS(arm64/x64);Grep固定携带微软@vscode/ripgrep-universal1.18.0的两架构原字节并核SHA,无员工PATH搜索或安装下载。主机需S0.4.11,N可选peer~0.4.4(缺失明确报能力缺项),L当时~0.7.18,本地接纳与D19工程路径已接,真实员工端门仍开放。无DSH/CLI修改、额外设置节或npm发布。
98
103
 
99
104
  ## 合并保留的 Codex D20 路线
100
105
 
@@ -130,3 +135,18 @@ D20(0.8.8开发记录):本地来源的Codex原生文件与命令通过固
130
135
  0.8.14 不把空菜单、非穷举 picker、可选 UI category/label 或同值配置通知当作额外发送门槛;每轮仍要求可靠模型入口与 fresh exact setter ACK。本轮完成后的配置变化只记录 warning,保留真实回复与 finish,并明确未获单独的实际执行型号回报。同一身份、代际与可信目标的 needCode/诊断变化只记 info;真实身份、代际、目标或能力失效仍撤回。每次关闭/取消保留 caller、字段前后值和首次观测原因(不等于根因)。新增配置、阈值、超时、重试为 0。
131
136
 
132
137
  0.8.16 不新增 Config、默认值、阈值、超时或重试。首因可见、旧代不重投、实际选择与绑定一致属于不可关闭的正确性不变量。目录完成结果不用于后续请求缓存;活跃租约仅保留更新观测的选择有效性证据,释放即撤登记。断连/错代仍保留原详情信封及未确认状态,仅按旧合同读 receipt 核验。`unconfirmed-<eventId>.json` 私有记录追加是否尝试及失败阶段/固定码,不能当作 L 的 ACK;员工可见原因沿官方错误消息,无新增面板。
138
+
139
+ ## 本机执行的核验位置(继承0.8.18,不可关闭)
140
+
141
+ L接纳每轮真实输入时核对实际目录和N端点/代次。X只在该代执行通道open时向S核对一次target,并继续验证员工执行器固定SHA和官方版本;原生注册发生在open完成之后。之后broker输入及每个native RPC只核对既有内存连接、工作区绑定和接纳事实,撤销来自已订阅的L连接/位置、N网络事件和执行SSH关闭。N实例/代次改变会重新接纳并建立新通道,旧流不会跨代迁移。
142
+
143
+ 这属于执行位置与已接纳输入的一致性不变量,没有关闭设置。已取得owned keyed live的会话失败先交还官方会话并显示“正在确认主机清理”,详情落盘及条件kill/list随后在原清理promise进行;preflight、resolve、未取得live、无key/单次模式及detach/shutdown保持原await语义;同一会话仍由原cleanupFailure/未知送达门阻止不安全重发。没有新增超时、重试、模型限制或中止检查。技术详情事件EMPLOYEE_EXECUTION_VALIDATION_POLICY列出同一核验位置。
144
+
145
+
146
+ ## 0.8.19 / aw.4 本机 MCP 与失败原因
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 的过滤来自固定原生协议,不是可调阈值。
149
+
150
+ aw.4 仅在客户端声明 closeReason:1 后发送白名单 close.reason;旧端形状保持兼容。新版 X 接到明确远端 close 后释放员工 carrier,保留在途主机 session/new 回应,再由原本收尾释放 broker;非预期 EOF 仍按既有路径关闭。技术详情分别显示运行原因、首次观察和远端固定关闭原因;它们不因详情保存或清理失败而被替代。成功 connection-confirm 按阶段汇总次数和耗时,失败仍即时记录;实际确认操作不减少,无新增超时、重试、慢阈值或中止检查。
151
+
152
+ 03:44 现场的 stdio 首因仍属强推断,主机当时原码没有落盘;离线回放不能补造现场证据。aw.4 由编排审后交 S 安装,客户端固定包不由 X 发布;真实 leo Sol/Luna“你好”及写文件验收单独记录,不能用离线或包门代替。
package/dist/acp.d.ts CHANGED
@@ -18,6 +18,7 @@ export interface AcpHooks {
18
18
  onNeed?: (need: Need) => void;
19
19
  onDetail?: (detail: Readonly<SanitizedObservation>) => void;
20
20
  broker?: BrokerTransport;
21
+ onEmployeeInfo?: (summary: string) => void;
21
22
  employeeExecution?: (params: unknown, signal?: AbortSignal) => Promise<Record<string, unknown>>;
22
23
  }
23
24
  export interface EmployeeExecutionMeta {
@@ -26,6 +27,7 @@ export interface EmployeeExecutionMeta {
26
27
  workspaceId: string;
27
28
  cwd: string;
28
29
  acpMaxMessageBytes: number;
30
+ httpMcpPolicy?: 'reject' | 'ignore';
29
31
  }
30
32
  export interface AcpStartContext {
31
33
  systemPrompt?: string;
@@ -76,6 +78,7 @@ export declare class AcpSession {
76
78
  private stage;
77
79
  get diagnostics(): readonly Readonly<SanitizedObservation>[];
78
80
  private detail;
81
+ private employeeInfo;
79
82
  private need;
80
83
  private checkReporting;
81
84
  constructor(transport: acp.Stream | acp.AgentApp | stable.AgentApp, hooks: AcpHooks);
package/dist/acp.js CHANGED
@@ -1,4 +1,6 @@
1
- import { CloseDiagnostics } from "./close-diagnostics.js";
1
+ import { employeeExecutionInfo, employeeMcpSummary } from "./employee-info.js";
2
+ import { employeeAdapterCodes, employeeOpenCauseCodes } from "./employee-reasons.js";
3
+ import { CloseDiagnostics, withFailureEvidence } from "./close-diagnostics.js";
2
4
  import * as acp from '@agentclientprotocol/sdk/unstable';
3
5
  import { NeedError } from "./remote.js";
4
6
  import { employeeFailure } from "./presentation.js";
@@ -133,6 +135,15 @@ export class AcpSession {
133
135
  this.close({ layer: 'acp', initiator: 'acp.reporting-failure', reason: 'CONVERSATION_DETAILS_FAILED', action: 'close' });
134
136
  }
135
137
  }
138
+ employeeInfo(value) {
139
+ const info = employeeExecutionInfo(value);
140
+ if (!info)
141
+ return;
142
+ const summary = employeeMcpSummary(info);
143
+ 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
+ }
136
147
  need(need, hooks = this.hooks) {
137
148
  this.detail({ kind: 'need', severity: 'action-required', code: need.code, summary: need.message, payload: { requires: need.requires } }, hooks);
138
149
  hooks.onNeed?.(need);
@@ -255,7 +266,11 @@ export class AcpSession {
255
266
  if (!this.employeeNegotiated || !this.employeeMeta || !this.hooks.employeeExecution)
256
267
  throw new acp.RequestError(-32601, 'EMPLOYEE_EXECUTION_UNSUPPORTED');
257
268
  try {
258
- return await this.hooks.employeeExecution(params, signal);
269
+ const result = await this.hooks.employeeExecution(params, signal);
270
+ const request = record(params);
271
+ if (request?.operation === 'close' && result.closed === true)
272
+ this.employeeInfo(request.info);
273
+ return result;
259
274
  }
260
275
  catch (error) {
261
276
  if (error instanceof NeedError) {
@@ -390,8 +405,8 @@ export class AcpSession {
390
405
  signal?.addEventListener('abort', abort, { once: true });
391
406
  try {
392
407
  const hello = await this.stage('acp-initialize', () => this.connection.agent.request(acp.methods.agent.initialize, {
393
- protocolVersion: acp.PROTOCOL_VERSION, clientInfo: { name: '@ai-workbench/remote-agent', version: '0.8.17' },
394
- clientCapabilities: { fs: { readTextFile: false, writeTextFile: false }, terminal: false, session: { notices: {} }, ...(this.employeeMeta ? { _meta: { awEmployeeExecution: { version: 1 } } } : {}) },
408
+ protocolVersion: acp.PROTOCOL_VERSION, clientInfo: { name: '@ai-workbench/remote-agent', version: '0.8.19' },
409
+ clientCapabilities: { fs: { readTextFile: false, writeTextFile: false }, terminal: false, session: { notices: {} }, ...(this.employeeMeta ? { _meta: { awEmployeeExecution: { version: 1, closeReason: 1 } } } : {}) },
395
410
  }, { cancellationSignal: signal }));
396
411
  this.hooks.broker?.acknowledge(acp.methods.agent.initialize);
397
412
  if (hello.protocolVersion !== acp.PROTOCOL_VERSION)
@@ -403,7 +418,7 @@ export class AcpSession {
403
418
  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)
404
419
  throw new NeedError('EMPLOYEE_EXECUTION_UNSUPPORTED', '当前主机尚不支持在你的电脑执行Codex原生工具,本轮没有执行。', 'Codex员工端原生执行适配器与明确能力声明');
405
420
  this.employeeNegotiated = true;
406
- this.detail({ kind: 'session', severity: 'info', code: 'EMPLOYEE_EXECUTION_SELECTED', summary: '文件和命令在员工电脑的选定目录执行,Git使用本机仓库;模型、登录和全局助手要求仍在主机。本机MCP、hooks、plugins、冷恢复、快捷命令和主机文件报告尚不支持。外层ACP冷恢复已拒绝;原生子代理回收/缺少存活父会话时的恢复仅做源码检查,未实测。', payload: { bindingId: this.employeeMeta.bindingId, workspaceId: this.employeeMeta.workspaceId, cwd: this.employeeMeta.cwd, nativeCliVersion: '0.160.1', noHostFallback: true, agentConfigSource: 'host CODEX_HOME', unsupportedLocalFeatures: ['mcp', 'hooks', 'plugins', 'cold-resume', 'slash-commands', 'host-file-reports'] } });
421
+ this.detail({ kind: 'session', severity: 'info', code: 'EMPLOYEE_EXECUTION_SELECTED', summary: '文件和命令在员工电脑的选定目录执行,Git使用本机仓库;模型、登录和全局助手要求仍在主机。本机stdio MCP按原生规则忽略;HTTP MCP按管理员设置拒绝或忽略。本机hooks、plugins、冷恢复、快捷命令和主机文件报告尚不支持。外层ACP冷恢复已拒绝;原生子代理回收/缺少存活父会话时的恢复仅做源码检查,未实测。', 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'] } });
407
422
  }
408
423
  const systemPrompt = context?.systemPrompt;
409
424
  this.codexNewSessionOnly = !!context?.codexNewSessionOnly;
@@ -433,6 +448,8 @@ export class AcpSession {
433
448
  else {
434
449
  const result = await this.stage('acp-session-new', () => this.connection.agent.request(acp.methods.agent.session.new, params, { cancellationSignal: signal }));
435
450
  this.sessionId = result.sessionId;
451
+ if (this.employeeMeta)
452
+ this.employeeInfo(result._meta?.awEmployeeExecutionInfo);
436
453
  this.lifecycleConfiguration(result, configRevision);
437
454
  this.method = acp.methods.agent.session.new;
438
455
  }
@@ -482,6 +499,8 @@ export class AcpSession {
482
499
  if (!result.sessionId || result.sessionId === oldId)
483
500
  throw new NeedError('SESSION_STATE_INVALID', '新会话没有返回独立的远端标识,原会话状态需确认', '独立的新远端会话标识');
484
501
  this.sessionId = result.sessionId;
502
+ if (this.employeeMeta)
503
+ this.employeeInfo(result._meta?.awEmployeeExecutionInfo);
485
504
  this.lifecycleConfiguration(result, revision);
486
505
  this.hooks.broker?.acknowledge(acp.methods.agent.session.new);
487
506
  this.checkReporting();
@@ -609,8 +628,18 @@ export function acpNeed(error) {
609
628
  if (typeof value !== 'string')
610
629
  continue;
611
630
  const code = value.split(':', 1)[0];
612
- if (employeeAdapterCodes.has(code))
613
- return new NeedError(code, employeeFailure(code), `员工电脑执行适配器明确拒绝:${code};RPC 错误码 ${error.code}`);
631
+ if (employeeAdapterCodes.has(code)) {
632
+ const failure = new NeedError(code, employeeFailure(code), `员工电脑执行适配器明确拒绝:${code};RPC 错误码 ${error.code}`);
633
+ if (code === 'AW_EMPLOYEE_ENVIRONMENT_REGISTRATION_FAILED' && value.startsWith(code + ': ')) {
634
+ const reason = value.slice(code.length + 2);
635
+ if (employeeAdapterCodes.has(reason) || employeeOpenCauseCodes.has(reason)) {
636
+ const retained = withFailureEvidence(Object.assign(failure, { cause: new NeedError(reason, '员工执行通道开通失败', '核对固定开通原因') }), [], 'diagnostic');
637
+ retained.requires += `;RPC 错误码 ${error.code}`;
638
+ return retained;
639
+ }
640
+ }
641
+ return failure;
642
+ }
614
643
  }
615
644
  }
616
645
  if (error instanceof acp.RequestError && error.code === -32000)
@@ -622,4 +651,3 @@ export function acpNeed(error) {
622
651
  function record(value) { return value !== null && typeof value === 'object' && !Array.isArray(value) ? value : undefined; }
623
652
  const knownUpdates = new Set(['user_message_chunk', 'agent_message_chunk', 'agent_thought_chunk', 'tool_call', 'tool_call_update', 'plan', 'plan_update', 'plan_removed', 'available_commands_update', 'current_mode_update', 'config_option_update', 'session_info_update', 'usage_update', 'notice', 'compaction_update', 'compaction_summary_chunk', 'subagent_update', 'session_message', 'session_message_chunk']);
624
653
  function safeLabel(value) { return value.replaceAll(/[\r\n\x00-\x1f\x7f]/g, ' '); }
625
- const employeeAdapterCodes = new Set(["AW_EMPLOYEE_ADDITIONAL_DIRECTORIES_UNSUPPORTED", "AW_EMPLOYEE_SLASH_COMMAND_UNSUPPORTED", "AW_EMPLOYEE_HOST_FILE_REPORT_UNSUPPORTED", "AW_EMPLOYEE_CONFIG_DISCOVERY_OVERRIDE_UNSUPPORTED", "AW_EMPLOYEE_CONFIG_ADMISSION_INVALID", "AW_EMPLOYEE_CONFIG_ADMISSION_FAILED", "AW_EMPLOYEE_HOST_PROJECT_CONFIG_UNSUPPORTED", "AW_EMPLOYEE_EXECUTOR_MCP_UNSUPPORTED", "AW_EMPLOYEE_EXECUTOR_CONFIG_PROTOCOL_INVALID", "AW_EMPLOYEE_EXECUTOR_CONFIG_READ_FAILED", "AW_EMPLOYEE_AGENT_CONFIG_UNAVAILABLE", "AW_EMPLOYEE_BINDING_INVALID", "AW_EMPLOYEE_BINDING_MISMATCH", "AW_EMPLOYEE_CAPABILITY_REQUIRED", "AW_EMPLOYEE_CLOSE_FAILED", "AW_EMPLOYEE_CLOSE_RESPONSE_INVALID", "AW_EMPLOYEE_COLD_RESUME_UNSUPPORTED", "AW_EMPLOYEE_CWD_MISMATCH", "AW_EMPLOYEE_ENVIRONMENT_REGISTRATION_FAILED", "AW_EMPLOYEE_ENVIRONMENT_REQUIRED", "AW_EMPLOYEE_EXECUTION_CLOSED", "AW_EMPLOYEE_EXECUTION_ID_REQUIRED", "AW_EMPLOYEE_HOST_AUXILIARY_CONFIG_UNSUPPORTED", "AW_EMPLOYEE_HOST_AUXILIARY_UNSUPPORTED", "AW_EMPLOYEE_HOST_FALLBACK_FORBIDDEN", "AW_EMPLOYEE_MCP_UNSUPPORTED", "AW_EMPLOYEE_MESSAGE_INVALID", "AW_EMPLOYEE_MESSAGE_TOO_LARGE", "AW_EMPLOYEE_OPEN_RESPONSE_INVALID", "AW_EMPLOYEE_READ_MESSAGE_INVALID", "AW_EMPLOYEE_READ_RESPONSE_INVALID", "AW_EMPLOYEE_RELAY_ADDRESS_INVALID", "AW_EMPLOYEE_RELAY_FAILED", "AW_EMPLOYEE_REQUEST_INVALID", "AW_EMPLOYEE_SESSION_UNKNOWN", "AW_EMPLOYEE_WIRE_FAILED", "AW_EMPLOYEE_WRITE_RESPONSE_INVALID"]);
@@ -1,6 +1,7 @@
1
1
  import type { RequestPermissionRequest } from '@agentclientprotocol/sdk/unstable';
2
+ import type { HostWorkspacePresentation } from './input.ts';
2
3
  /** ACP diff.oldText=null means a new file. Write alone may overwrite a file. */
3
- export declare function approvalLabel(request: RequestPermissionRequest, location?: 'host' | 'local'): {
4
+ export declare function approvalLabel(request: RequestPermissionRequest, location?: 'host' | 'local', hostWorkspace?: HostWorkspacePresentation): {
4
5
  toolName: string;
5
6
  reason: string;
6
7
  } | undefined;
@@ -5,7 +5,7 @@ function display(value) {
5
5
  return safe && !safe.includes('[已脱敏:') ? safe : undefined;
6
6
  }
7
7
  /** ACP diff.oldText=null means a new file. Write alone may overwrite a file. */
8
- export function approvalLabel(request, location = 'host') {
8
+ export function approvalLabel(request, location = 'host', hostWorkspace) {
9
9
  const tool = request.toolCall;
10
10
  const diffs = tool.content?.filter(item => item.type === 'diff') ?? [];
11
11
  const originalName = tool.name ?? tool._meta?.claudeCode?.toolName ?? /^(Write|Edit|MultiEdit) /.exec(tool.title ?? '')?.[1];
@@ -53,5 +53,13 @@ export function approvalLabel(request, location = 'host') {
53
53
  return;
54
54
  if (!safePaths.length && !description)
55
55
  return;
56
+ if (location === 'host' && hostWorkspace && (tool.kind === 'execute' || name === 'Bash' || name === 'exec_command')) {
57
+ const cwd = display(hostWorkspace.cwd);
58
+ if (!cwd)
59
+ return;
60
+ // ACP locations are accessed paths, not a cwd declaration. Preserve them as
61
+ // reported targets, alongside the independently resolved session directory.
62
+ return { toolName: action, reason: action + '(会话主机目录:' + cwd + ')' + (safePaths.length ? ';工具报告目标:' + safePaths.join('、') : '') + (description ? ':' + description : '') };
63
+ }
56
64
  return { toolName: action, reason: action + (safePaths.length ? ' ' + safePaths.join('、') : '') + (description ? ':' + description : '') };
57
65
  }
package/dist/client.js CHANGED
@@ -1,7 +1,7 @@
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
- const messages={"en":{"administratorNote":"Settings → Built-in plugins → 远程助手参数. Seven 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}];
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"}];
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;
@@ -12,7 +12,7 @@ export interface CloseCause {
12
12
  operation?: string;
13
13
  exitCode?: number | null;
14
14
  signal?: string | null;
15
- remoteReason?: 'unavailable';
15
+ remoteReason?: string;
16
16
  changedFields?: readonly string[];
17
17
  changes?: readonly CloseChange[];
18
18
  }
@@ -1,3 +1,5 @@
1
+ import { employeeExecutionInfo } from "./employee-info.js";
2
+ import { trustedEmployeeReason } from "./employee-reasons.js";
1
3
  import { randomUUID } from 'node:crypto';
2
4
  import { STAGES } from "./stage-timing.js";
3
5
  const redactionMarkers = new WeakSet();
@@ -260,11 +262,13 @@ function timingStructure(value) {
260
262
  if (!value || typeof value !== 'object' || Array.isArray(value))
261
263
  return opaqueStructure(value);
262
264
  return Object.fromEntries(Object.entries(value).map(([key, child]) => {
265
+ if (key === 'count' && typeof child === 'number' && Number.isSafeInteger(child) && child >= 1)
266
+ return [key, child];
263
267
  if (key === 'phase' && typeof child === 'string' && Object.hasOwn(STAGES, child))
264
268
  return [key, child];
265
269
  if (key === 'clock' && child === 'client-monotonic' || key === 'outcome' && (child === 'ok' || child === 'failed'))
266
270
  return [key, child];
267
- if (['durationMs', 'elapsedMs'].includes(key) && typeof child === 'number' && Number.isFinite(child) && child >= 0)
271
+ if (['durationMs', 'elapsedMs', 'totalDurationMs', 'minDurationMs', 'maxDurationMs'].includes(key) && typeof child === 'number' && Number.isFinite(child) && child >= 0)
268
272
  return [key, child];
269
273
  return [key, opaqueStructure(child)];
270
274
  }));
@@ -361,7 +365,7 @@ function closeStructure(value) {
361
365
  return [key, child];
362
366
  if (key === 'reason' && typeof child === 'string' && /^[A-Z][A-Z0-9_]*$/.test(child))
363
367
  return [key, child];
364
- if (key === 'remoteReason' && child === 'unavailable' || key === 'first' && typeof child === 'boolean')
368
+ if (key === 'remoteReason' && (child === 'unavailable' || trustedEmployeeReason(child) !== undefined) || key === 'first' && typeof child === 'boolean')
365
369
  return [key, child];
366
370
  if (['closeSequence', 'elapsedMs'].includes(key) && typeof child === 'number' && Number.isFinite(child) && child >= 0)
367
371
  return [key, child];
@@ -400,11 +404,12 @@ export function sanitizeObservation(observation, protectModelMaterial = false) {
400
404
  : observation.kind === 'session' && ['REMOTE_CLOSE_FIRST', 'REMOTE_CLOSE_NEXT'].includes(observation.code) ? closeStructure(payload)
401
405
  : observation.kind === 'commands' ? catalogStructure(payload)
402
406
  : observation.kind === 'usage' ? usageStructure(payload)
403
- : observation.kind === 'session' && observation.code === 'REMOTE_STAGE_TIMING' ? timingStructure(payload)
404
- : observation.kind === 'session' && ['CLAUDE_EMPLOYEE_OPERATION', 'CLAUDE_EMPLOYEE_PROCESS', 'CLAUDE_EMPLOYEE_REVOKED', 'CLAUDE_EMPLOYEE_SCOPE'].includes(observation.code) ? employeeStructure(payload)
405
- : observation.kind === 'need' ? payload
406
- : observation.kind === 'tool' || ['SDK_ITEM', 'SDK_TOOL', 'ACP_SESSION_UPDATE', 'ACP_IDLE_UPDATE'].includes(observation.code) ? toolStructure(payload)
407
- : opaqueStructure(payload),
407
+ : observation.kind === 'session' && observation.code === 'EMPLOYEE_MCP_INFORMATION' ? (employeeExecutionInfo(observation.payload) ?? { information: 'unconfirmed' })
408
+ : observation.kind === 'session' && observation.code === 'REMOTE_STAGE_TIMING' ? timingStructure(payload)
409
+ : observation.kind === 'session' && ['CLAUDE_EMPLOYEE_OPERATION', 'CLAUDE_EMPLOYEE_PROCESS', 'CLAUDE_EMPLOYEE_REVOKED', 'CLAUDE_EMPLOYEE_SCOPE'].includes(observation.code) ? employeeStructure(payload)
410
+ : observation.kind === 'need' ? payload
411
+ : observation.kind === 'tool' || ['SDK_ITEM', 'SDK_TOOL', 'ACP_SESSION_UPDATE', 'ACP_IDLE_UPDATE'].includes(observation.code) ? toolStructure(payload)
412
+ : opaqueStructure(payload),
408
413
  });
409
414
  }
410
415
  function validatedScope(scope) {
@@ -34,6 +34,11 @@ export declare class EmployeeExecution {
34
34
  private ended;
35
35
  private error?;
36
36
  private pendingRead;
37
+ private remoteClosure?;
38
+ /** Set only after the existing binding and execution ID checks accept a close. */
39
+ get remoteClose(): Readonly<{
40
+ reason?: string;
41
+ }> | undefined;
37
42
  private writeTail;
38
43
  private queuedWriteBytes;
39
44
  private closing;
@@ -1,3 +1,4 @@
1
+ import { trustedEmployeeReason } from "./employee-reasons.js";
1
2
  import { closeCode } from "./close-diagnostics.js";
2
3
  import { randomUUID } from 'node:crypto';
3
4
  import { NeedError } from "./remote.js";
@@ -49,6 +50,9 @@ export class EmployeeExecution {
49
50
  ended = false;
50
51
  error;
51
52
  pendingRead = false;
53
+ remoteClosure;
54
+ /** Set only after the existing binding and execution ID checks accept a close. */
55
+ get remoteClose() { return this.remoteClosure; }
52
56
  writeTail = Promise.resolve();
53
57
  queuedWriteBytes = 0;
54
58
  closing = Promise.resolve();
@@ -60,7 +64,9 @@ export class EmployeeExecution {
60
64
  this.options = options;
61
65
  }
62
66
  parse(params) {
63
- if (!object(params) || Object.keys(params).some(key => !['version', 'bindingId', 'operation', 'executionId', 'message'].includes(key)) || params.version !== 1 || typeof params.bindingId !== 'string' || !['open', 'write', 'read', 'close'].includes(params.operation))
67
+ if (!object(params) || Object.keys(params).some(key => !['version', 'bindingId', 'operation', 'executionId', 'message', 'reason', 'info'].includes(key)) || params.version !== 1 || typeof params.bindingId !== 'string' || !['open', 'write', 'read', 'close'].includes(params.operation))
68
+ throw need('EMPLOYEE_EXEC_PROTOCOL_INVALID');
69
+ if (params.operation !== 'close' && (Object.hasOwn(params, 'reason') || Object.hasOwn(params, 'info')))
64
70
  throw need('EMPLOYEE_EXEC_PROTOCOL_INVALID');
65
71
  if (params.bindingId !== this.options.bindingId)
66
72
  throw need('EMPLOYEE_EXEC_IDENTITY_MISMATCH');
@@ -117,7 +123,11 @@ export class EmployeeExecution {
117
123
  async handleRequest(request, signal, observe) {
118
124
  cancelled(signal);
119
125
  if (request.operation === 'close') {
120
- this.stop(undefined, { layer: 'executor', initiator: 'remote.employee-exec.close', reason: 'REMOTE_REASON_UNAVAILABLE', remoteReason: 'unavailable', operation: 'close', action: 'close' }, observe);
126
+ const reason = trustedEmployeeReason(request.reason);
127
+ this.remoteClosure ??= Object.freeze({});
128
+ if (reason && !this.remoteClosure.reason)
129
+ this.remoteClosure = Object.freeze({ reason });
130
+ this.stop(undefined, { layer: 'executor', initiator: 'remote.employee-exec.close', reason: reason ?? 'REMOTE_REASON_UNAVAILABLE', remoteReason: reason ?? 'unavailable', operation: 'close', action: 'close' }, observe);
121
131
  await this.cleanup();
122
132
  return { version: 1, closed: true, result: 'UNKNOWN' };
123
133
  }
@@ -0,0 +1,13 @@
1
+ /** Optional protocol information only. Malformed metadata cannot stop a turn. */
2
+ export interface EmployeeExecutionInfo {
3
+ version: 1;
4
+ httpMcpPolicy: 'reject' | 'ignore';
5
+ ignoredStdio: string[];
6
+ ignoredHttp: string[];
7
+ ignoredNativeHttp: string[];
8
+ ignoredStdioUnshown: number;
9
+ ignoredHttpUnshown: number;
10
+ ignoredNativeHttpUnshown: number;
11
+ }
12
+ export declare function employeeExecutionInfo(value: unknown): EmployeeExecutionInfo | undefined;
13
+ export declare function employeeMcpSummary(info: EmployeeExecutionInfo): string;
@@ -0,0 +1,28 @@
1
+ const object = (value) => value !== null && typeof value === 'object' && !Array.isArray(value);
2
+ const safeName = (value) => typeof value === 'string' && value.length > 0 && !/[\p{C}\r\n]/u.test(value);
3
+ export function employeeExecutionInfo(value) {
4
+ if (!object(value) || value.version !== 1 || (value.httpMcpPolicy !== 'reject' && value.httpMcpPolicy !== 'ignore'))
5
+ return;
6
+ const lists = {};
7
+ for (const key of ['ignoredStdio', 'ignoredHttp', 'ignoredNativeHttp']) {
8
+ const entries = value[key];
9
+ if (!Array.isArray(entries))
10
+ return;
11
+ lists[key] = entries.filter(safeName);
12
+ const unshown = value[key + 'Unshown'];
13
+ lists[key + 'Unshown'] = (Number.isSafeInteger(unshown) && Number(unshown) >= 0 ? Number(unshown) : 0) + entries.filter(entry => !safeName(entry)).length;
14
+ }
15
+ return { version: 1, httpMcpPolicy: value.httpMcpPolicy, ...lists };
16
+ }
17
+ // Code-span delimiter exceeds every backtick run, so names cannot inject Markdown/HTML.
18
+ const quoted = (names) => names.map(name => { let longest = 0; for (const run of name.matchAll(/`+/g))
19
+ longest = Math.max(longest, run[0].length); const delimiter = '`'.repeat(longest + 1); return delimiter + ' ' + name + ' ' + delimiter; }).join('、');
20
+ export function employeeMcpSummary(info) {
21
+ const parts = [];
22
+ for (const [key, label] of [['ignoredStdio', '已忽略本机 stdio MCP'], ['ignoredHttp', info.httpMcpPolicy === 'ignore' ? '已按设置忽略本机 HTTP MCP' : '本机 HTTP MCP 未获支持,当前设置为拒绝'], ['ignoredNativeHttp', '已按原生能力忽略本机 HTTP MCP']]) {
23
+ const names = info[key], unshown = info[key + 'Unshown'];
24
+ if (names.length || unshown)
25
+ parts.push(label + ': ' + [...(names.length ? [quoted(names)] : []), ...(unshown ? [`${unshown} 个名称含不可显示内容`] : [])].join(';'));
26
+ }
27
+ return parts.join('。');
28
+ }
@@ -8,11 +8,11 @@ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExte
8
8
  };
9
9
  import { connectNeedError } from "./connect-need.js";
10
10
  import { CODEX_EMPLOYEE_SCOPE_NOTICE } from "./employee-presentation.js";
11
- import { CloseDiagnostics, withFailureEvidence, closeCode, closeErrorCodes } from "./close-diagnostics.js";
11
+ import { CloseDiagnostics, withFailureEvidence, withFirstFailureEvidence, closeCode, closeErrorCodes } from "./close-diagnostics.js";
12
12
  import { createHash } from 'node:crypto';
13
13
  import { admitEmployeeLocation } from "./employee-location.js";
14
14
  import { EmployeeExecution } from "./employee-exec.js";
15
- import { createEmployeeSshTransport, validateEmployeeSshTarget } from "./employee-ssh.js";
15
+ import { createEmployeeSshTransport } from "./employee-ssh.js";
16
16
  import { sshIo } from "./broker.js";
17
17
  import { NeedError } from "./remote.js";
18
18
  const fail = (code) => new NeedError(code, code === 'EMPLOYEE_OPERATION_UNSUPPORTED' ? '当前本机工作区只支持正在连接的会话;单次运行和断线后的冷恢复暂不支持,请新建会话。' : '本机工作区的执行连接无法继续(' + code + '),本轮没有改用主机执行。', '当前 L 工作区绑定、N/S 员工执行端点与支持本机执行的会话适配器');
@@ -177,17 +177,25 @@ export function createEmployeeProvider(get, options, agent) {
177
177
  await prior.dispose(fail('EMPLOYEE_CONTEXT_CHANGED'), { layer: 'endpoint', initiator: 'provider.scope-change', reason: 'EMPLOYEE_CONTEXT_CHANGED', action: 'dispose' });
178
178
  let entry = prior && prior.scope === scope && !prior.ended ? prior : undefined;
179
179
  let candidate = entry;
180
+ // Admission still verifies the real directory and N generation. Per-RPC gates below
181
+ // reuse the subscribed connection/location/network facts, without another SSH.
182
+ await admitted.validate(request.signal ?? new AbortController().signal);
183
+ details?.record({ kind: 'session', severity: 'info', code: 'EMPLOYEE_EXECUTION_VALIDATION_POLICY', summary: '本机执行核验不可关闭:L接纳核对实际目录与N端点;同代执行通道仅在open时核对一次主机target与固定执行器。之后每条消息只核对内存中的连接、工作区绑定和已接纳输入,撤销沿现有连接/位置/N网络事件及执行SSH关闭处理;不重复SSH,不改用主机执行。' });
184
+ if (entry)
185
+ entry.current = input;
180
186
  const base = async (signal) => {
181
187
  signal.throwIfAborted();
182
188
  if (get() !== api || employee.getNetwork() !== network)
183
189
  throw fail('EMPLOYEE_CONTEXT_CHANGED');
184
190
  if (candidate?.ended)
185
191
  throw candidate.reason ?? fail('EMPLOYEE_EXEC_CLOSED');
186
- await (candidate?.current ?? input).stable(signal);
187
- await closeDiagnostics.run('endpoint.prepare', () => admitted.validate(signal));
192
+ const current = candidate?.current ?? input;
193
+ current.request.localAdmissionCheck?.();
194
+ await current.stable(signal);
188
195
  if (get() !== api || employee.getNetwork() !== network)
189
196
  throw fail('EMPLOYEE_CONTEXT_CHANGED');
190
- await (candidate?.current ?? input).stable(signal);
197
+ current.request.localAdmissionCheck?.();
198
+ await current.stable(signal);
191
199
  signal.throwIfAborted();
192
200
  };
193
201
  let argv = employee.argv;
@@ -202,17 +210,17 @@ export function createEmployeeProvider(get, options, agent) {
202
210
  const scopedIo = { run: (command, signal) => io.run(command, signal), attach: async (command) => {
203
211
  const observe = closeDiagnostics.capture();
204
212
  const child = await io.attach(command);
205
- child.once('close', (code, signal) => { observe({ layer: 'relay', initiator: 'ssh.process.close', reason: 'EMPLOYEE_EXEC_UNKNOWN', action: 'exit', exitCode: code, signal }); if (candidate && !candidate.ended)
213
+ child.once('close', (code, signal) => { observe({ layer: 'relay', initiator: 'ssh.process.close', reason: 'EMPLOYEE_EXEC_UNKNOWN', action: 'exit', exitCode: code, signal }); if (candidate && !candidate.ended && !candidate.execution.remoteClose)
206
214
  void candidate.dispose(fail('EMPLOYEE_EXEC_UNKNOWN'), { layer: 'runtime', initiator: 'provider.child-close', reason: 'EMPLOYEE_EXEC_UNKNOWN', action: 'dispose' }).catch(() => { }); });
207
215
  return child;
208
216
  } };
209
217
  const ssh = { closeDiagnostics, target, endpoint: admitted.endpoint, employeeBinary: native.path, binarySha256: native.sha256, acpMaxMessageBytes: request.acpMaxMessageBytes, argv: argv ?? (() => []), validate: base, io: scopedIo };
210
218
  const validate = async (signal) => { try {
211
- await validateEmployeeSshTarget(ssh, signal);
219
+ await base(signal);
212
220
  }
213
221
  catch (error) {
214
222
  if (candidate && errorCode(error) !== 'ABORTED')
215
- await candidate.dispose(asNeed(error), { layer: 'endpoint', initiator: 'provider.validation-failure', reason: closeCode(error), action: 'invalidate' });
223
+ void candidate.dispose(asNeed(error), { layer: 'endpoint', initiator: 'provider.validation-failure', reason: closeCode(error), action: 'invalidate' }).catch(cleanup => closeDiagnostics.record({ layer: 'runtime', initiator: 'provider.validation-cleanup', reason: closeCode(cleanup, 'REMOTE_CLEANUP_UNCONFIRMED'), action: 'failure' }));
216
224
  throw error;
217
225
  } };
218
226
  await validate(request.signal ?? new AbortController().signal);
@@ -247,7 +255,8 @@ export function createEmployeeProvider(get, options, agent) {
247
255
  }
248
256
  const disposers = [];
249
257
  const execution = new EmployeeExecution({ closeDiagnostics, bindingId: admitted.bindingId, acpMaxMessageBytes: request.acpMaxMessageBytes, validate, transport: signal => createEmployeeSshTransport(ssh, signal) });
250
- 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 }, 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 ?? 'reject' }, employeePresentation: CODEX_EMPLOYEE_SCOPE_NOTICE };
259
+ runtime.employeeRemoteCloseReason = () => execution.remoteClose?.reason;
251
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' }) => {
252
261
  closeDiagnostics.record({ ...cause, changes: cause.changes ?? [{ field: 'provider.ended', before: entry.ended, after: true }] });
253
262
  if (entry.closing)
@@ -302,15 +311,15 @@ export function createEmployeeProvider(get, options, agent) {
302
311
  runtime.employeeExecution = async (params, signal) => {
303
312
  try {
304
313
  const result = await execution.handle(params, signal);
305
- if (result.closed)
306
- await entry.dispose(undefined, { layer: 'runtime', initiator: 'provider.rpc-closed-result', reason: 'EMPLOYEE_EXEC_CLOSED', operation: typeof params === 'object' && params && 'operation' in params && typeof params.operation === 'string' ? params.operation : 'unknown', action: 'dispose' });
314
+ if (result.closed && !execution.remoteClose)
315
+ void entry.dispose(undefined, { layer: 'runtime', initiator: 'provider.rpc-closed-result', reason: 'EMPLOYEE_EXEC_CLOSED', operation: typeof params === 'object' && params && 'operation' in params && typeof params.operation === 'string' ? params.operation : 'unknown', action: 'dispose' }).catch(cleanup => closeDiagnostics.record({ layer: 'runtime', initiator: 'provider.rpc-cleanup', reason: closeCode(cleanup, 'REMOTE_CLEANUP_UNCONFIRMED'), action: 'failure' }));
307
316
  return { ...result };
308
317
  }
309
318
  catch (error) {
310
319
  // EmployeeExecution reserves ABORTED for an unconsumed read wait. Lost responses are UNKNOWN.
311
- if (!(params && typeof params === 'object' && 'operation' in params && params.operation === 'read' && errorCode(error) === 'ABORTED'))
312
- await entry.dispose(asNeed(error), { layer: 'runtime', initiator: 'provider.rpc-failure', reason: closeCode(error), action: 'dispose' });
313
- throw error;
320
+ if (!execution.remoteClose && !(params && typeof params === 'object' && 'operation' in params && params.operation === 'read' && errorCode(error) === 'ABORTED'))
321
+ void entry.dispose(asNeed(error), { layer: 'runtime', initiator: 'provider.rpc-failure', reason: closeCode(error), action: 'dispose' }).catch(cleanup => closeDiagnostics.record({ layer: 'runtime', initiator: 'provider.rpc-cleanup', reason: closeCode(cleanup, 'REMOTE_CLEANUP_UNCONFIRMED'), action: 'failure' }));
322
+ throw withFirstFailureEvidence(error, closeDiagnostics.first?.reason, errorCode(error));
314
323
  }
315
324
  };
316
325
  runtime.dispose = cause => entry.dispose(undefined, cause ?? { layer: 'runtime', initiator: 'runtime.dispose', reason: 'RUNTIME_DISPOSE', action: 'dispose' });
@@ -0,0 +1,4 @@
1
+ /** Approved protocol codes only; remote exception text is never a reason. */
2
+ export declare const employeeAdapterCodes: Set<string>;
3
+ export declare const employeeOpenCauseCodes: Set<string>;
4
+ export declare function trustedEmployeeReason(value: unknown): string | undefined;