@ai-workbench/remote-agent 0.8.18 → 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.18` 发布形态包。继承0.8.17主机来源执行与L具体Need透传;本版修复Codex员工执行通道时序(需受保护aw.3)、去除逐RPC重复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.18,由X按编排裁定在X2的0.8.17上交付aw.3/去重复SSH/失败先交还;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.18或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
 
@@ -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 | ignore | reject / ignore;新连接生效。默认ignore仅剔除原生会采用的本机HTTP扩展投影后继续;可选reject保留固定拒绝原因。不改员工配置。忽略的HTTP扩展按名称用员工用语提示,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.21当前配置以开头、管理员八项表及末节为准;这些历史工程记录不替代本版产品验收。
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.18对齐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.21对齐L `~0.7.20`。D8模型目录双事件长期监听保留;同epoch的recovery提示/失败消息变化不触发执行状态失效,真实实例/epoch/phase/可信目标/登录变化仍失效。完整D9两品牌正文/状态/就绪门呈现按编排00:50允许的范围记下一版,本版不宣称已消费完成;L的connectionMessages.administratorAfterMs仍只由L管理;employeeHttpMcpPolicy自0.8.19新增,见后文;0.8.20无新增设置。
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
 
@@ -131,8 +136,39 @@ D20(0.8.8开发记录):本地来源的Codex原生文件与命令通过固
131
136
 
132
137
  0.8.16 不新增 Config、默认值、阈值、超时或重试。首因可见、旧代不重投、实际选择与绑定一致属于不可关闭的正确性不变量。目录完成结果不用于后续请求缓存;活跃租约仅保留更新观测的选择有效性证据,释放即撤登记。断连/错代仍保留原详情信封及未确认状态,仅按旧合同读 receipt 核验。`unconfirmed-<eventId>.json` 私有记录追加是否尝试及失败阶段/固定码,不能当作 L 的 ACK;员工可见原因沿官方错误消息,无新增面板。
133
138
 
134
- ### 本机执行的核验位置(不可关闭)
139
+ ## 本机执行的核验位置(继承0.8.18,不可关闭)
135
140
 
136
141
  L接纳每轮真实输入时核对实际目录和N端点/代次。X只在该代执行通道open时向S核对一次target,并继续验证员工执行器固定SHA和官方版本;原生注册发生在open完成之后。之后broker输入及每个native RPC只核对既有内存连接、工作区绑定和接纳事实,撤销来自已订阅的L连接/位置、N网络事件和执行SSH关闭。N实例/代次改变会重新接纳并建立新通道,旧流不会跨代迁移。
137
142
 
138
- 这属于执行位置与已接纳输入的一致性不变量,没有关闭设置。失败先交还官方会话并显示“正在确认主机清理”,详情落盘及条件kill/list随后在原清理promise进行;同一会话仍由原cleanupFailure/未知送达门阻止不安全重发。没有新增超时、重试、模型限制或中止检查。技术详情事件EMPLOYEE_EXECUTION_VALIDATION_POLICY列出同一核验位置。
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)不拒绝;0.8.20起仅在技术详情显示“已忽略本机 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“你好”及写文件验收单独记录,不能用离线或包门代替。
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,6 +16,7 @@ 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
22
  employeeExecution?: (params: unknown, signal?: AbortSignal) => Promise<Record<string, unknown>>;
@@ -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,9 @@ export declare class AcpSession {
76
78
  private stage;
77
79
  get diagnostics(): readonly Readonly<SanitizedObservation>[];
78
80
  private detail;
81
+ private employeeNoticeNames;
82
+ private employeeNoticeUnshown;
83
+ private employeeInfo;
79
84
  private need;
80
85
  private checkReporting;
81
86
  constructor(transport: acp.Stream | acp.AgentApp | stable.AgentApp, hooks: AcpHooks);
package/dist/acp.js CHANGED
@@ -1,3 +1,5 @@
1
+ import { employeeExecutionInfo, employeeMcpSummary, employeeHttpNotice } from "./employee-info.js";
2
+ import { employeeAdapterCodes, employeeOpenCauseCodes } from "./employee-reasons.js";
1
3
  import { CloseDiagnostics, withFailureEvidence } from "./close-diagnostics.js";
2
4
  import * as acp from '@agentclientprotocol/sdk/unstable';
3
5
  import { NeedError } from "./remote.js";
@@ -5,7 +7,7 @@ import { employeeFailure } from "./presentation.js";
5
7
  import { StageTiming } from "./stage-timing.js";
6
8
  import { isolateRpcIds } from "./rpc-namespace.js";
7
9
  import { recoveryCapabilities, supportsCodexContextPolicy } from "./recovery.js";
8
- import { sanitizeObservation } from "./conversation-details.js";
10
+ import { sanitizeObservation, sanitizeDisplayLabel } from "./conversation-details.js";
9
11
  import { ModelConfiguration, ModelConfigurationError } from "./model-configuration.js";
10
12
  /** ACP v1 with explicitly negotiated SDK 2.0 lifecycle/context extensions. */
11
13
  export class AcpSession {
@@ -133,6 +135,23 @@ export class AcpSession {
133
135
  this.close({ layer: 'acp', initiator: 'acp.reporting-failure', reason: 'CONVERSATION_DETAILS_FAILED', action: 'close' });
134
136
  }
135
137
  }
138
+ employeeNoticeNames = new Set();
139
+ employeeNoticeUnshown = false;
140
+ employeeInfo(value) {
141
+ const info = employeeExecutionInfo(value);
142
+ if (!info)
143
+ return;
144
+ const summary = employeeMcpSummary(info);
145
+ this.detail({ kind: 'session', severity: 'info', code: 'EMPLOYEE_MCP_INFORMATION', summary: summary || '本机MCP策略已应用:' + info.httpMcpPolicy, payload: info });
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
+ }
154
+ }
136
155
  need(need, hooks = this.hooks) {
137
156
  this.detail({ kind: 'need', severity: 'action-required', code: need.code, summary: need.message, payload: { requires: need.requires } }, hooks);
138
157
  hooks.onNeed?.(need);
@@ -255,7 +274,11 @@ export class AcpSession {
255
274
  if (!this.employeeNegotiated || !this.employeeMeta || !this.hooks.employeeExecution)
256
275
  throw new acp.RequestError(-32601, 'EMPLOYEE_EXECUTION_UNSUPPORTED');
257
276
  try {
258
- return await this.hooks.employeeExecution(params, signal);
277
+ const result = await this.hooks.employeeExecution(params, signal);
278
+ const request = record(params);
279
+ if (request?.operation === 'close' && result.closed === true)
280
+ this.employeeInfo(request.info);
281
+ return result;
259
282
  }
260
283
  catch (error) {
261
284
  if (error instanceof NeedError) {
@@ -370,6 +393,8 @@ export class AcpSession {
370
393
  if (this.busy || this.updating || this.selecting)
371
394
  throw new NeedError('TURN_BUSY', '上一轮或配置更新结束前不能更换会话钩子', '上一轮结束');
372
395
  this.hooks = hooks;
396
+ this.employeeNoticeNames.clear();
397
+ this.employeeNoticeUnshown = false;
373
398
  }
374
399
  get closed() { return this.connection.closed; }
375
400
  validateNewContext(text, requirePolicy = false) {
@@ -390,8 +415,8 @@ export class AcpSession {
390
415
  signal?.addEventListener('abort', abort, { once: true });
391
416
  try {
392
417
  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.18' },
394
- clientCapabilities: { fs: { readTextFile: false, writeTextFile: false }, terminal: false, session: { notices: {} }, ...(this.employeeMeta ? { _meta: { awEmployeeExecution: { version: 1 } } } : {}) },
418
+ protocolVersion: acp.PROTOCOL_VERSION, clientInfo: { name: '@ai-workbench/remote-agent', version: '0.8.21' },
419
+ clientCapabilities: { fs: { readTextFile: false, writeTextFile: false }, terminal: false, session: { notices: {} }, ...(this.employeeMeta ? { _meta: { awEmployeeExecution: { version: 1, closeReason: 1 } } } : {}) },
395
420
  }, { cancellationSignal: signal }));
396
421
  this.hooks.broker?.acknowledge(acp.methods.agent.initialize);
397
422
  if (hello.protocolVersion !== acp.PROTOCOL_VERSION)
@@ -403,7 +428,7 @@ export class AcpSession {
403
428
  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
429
  throw new NeedError('EMPLOYEE_EXECUTION_UNSUPPORTED', '当前主机尚不支持在你的电脑执行Codex原生工具,本轮没有执行。', 'Codex员工端原生执行适配器与明确能力声明');
405
430
  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'] } });
431
+ 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
432
  }
408
433
  const systemPrompt = context?.systemPrompt;
409
434
  this.codexNewSessionOnly = !!context?.codexNewSessionOnly;
@@ -433,6 +458,8 @@ export class AcpSession {
433
458
  else {
434
459
  const result = await this.stage('acp-session-new', () => this.connection.agent.request(acp.methods.agent.session.new, params, { cancellationSignal: signal }));
435
460
  this.sessionId = result.sessionId;
461
+ if (this.employeeMeta)
462
+ this.employeeInfo(result._meta?.awEmployeeExecutionInfo);
436
463
  this.lifecycleConfiguration(result, configRevision);
437
464
  this.method = acp.methods.agent.session.new;
438
465
  }
@@ -482,6 +509,8 @@ export class AcpSession {
482
509
  if (!result.sessionId || result.sessionId === oldId)
483
510
  throw new NeedError('SESSION_STATE_INVALID', '新会话没有返回独立的远端标识,原会话状态需确认', '独立的新远端会话标识');
484
511
  this.sessionId = result.sessionId;
512
+ if (this.employeeMeta)
513
+ this.employeeInfo(result._meta?.awEmployeeExecutionInfo);
485
514
  this.lifecycleConfiguration(result, revision);
486
515
  this.hooks.broker?.acknowledge(acp.methods.agent.session.new);
487
516
  this.checkReporting();
@@ -629,8 +658,6 @@ export function acpNeed(error) {
629
658
  return new NeedError('ACP_FAILED', '远端 ACP 请求失败,根因未查明;未切换到新会话', `有效的远端 ACP 连接;RPC 错误码 ${error.code}`);
630
659
  return new NeedError('ACP_FAILED', '远端 ACP 请求失败或连接中断,根因未查明;未切换到新会话', '有效的远端 ACP 连接');
631
660
  }
632
- const employeeOpenCauseCodes = new Set(['AW_EMPLOYEE_BINARY_UNAVAILABLE', 'AW_EMPLOYEE_BINARY_HASH_FAILED', 'AW_EMPLOYEE_BINARY_HASH_MISMATCH', 'AW_EMPLOYEE_BINARY_VERSION_FAILED', 'AW_EMPLOYEE_BINARY_VERSION_MISMATCH', 'EMPLOYEE_CONTEXT_CHANGED', 'EMPLOYEE_ENDPOINT_INVALID', 'EMPLOYEE_BINARY_REQUIRED', 'EMPLOYEE_EXECUTION_UNSUPPORTED', 'EMPLOYEE_EXEC_PROTOCOL_INVALID', 'EMPLOYEE_EXEC_IDENTITY_MISMATCH', 'EMPLOYEE_EXEC_CLOSED', 'EMPLOYEE_EXEC_UNKNOWN', 'EMPLOYEE_EXEC_ALREADY_OPEN', 'EMPLOYEE_EXEC_READ_BUSY', 'ACP_MESSAGE_TOO_LARGE', 'ABORTED', 'SETTINGS_REQUIRED']);
633
661
  function record(value) { return value !== null && typeof value === 'object' && !Array.isArray(value) ? value : undefined; }
634
662
  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']);
635
663
  function safeLabel(value) { return value.replaceAll(/[\r\n\x00-\x1f\x7f]/g, ' '); }
636
- 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
  }
@@ -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
- 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":"默认 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关闭,超限明确停止、不截断;整数必须能精确表示,防止消息缺失或容量计算失真。'],
@@ -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) {
@@ -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)
@@ -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
  }