@ai-workbench/remote-agent 0.8.18 → 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 +25 -11
- package/dist/acp.d.ts +3 -0
- package/dist/acp.js +23 -6
- package/dist/approval-label.d.ts +2 -1
- package/dist/approval-label.js +9 -1
- package/dist/client.js +2 -2
- package/dist/close-diagnostics.d.ts +1 -1
- package/dist/conversation-details.js +12 -7
- package/dist/employee-exec.d.ts +5 -0
- package/dist/employee-exec.js +12 -2
- package/dist/employee-info.d.ts +13 -0
- package/dist/employee-info.js +28 -0
- package/dist/employee-provider.js +5 -4
- package/dist/employee-reasons.d.ts +4 -0
- package/dist/employee-reasons.js +61 -0
- package/dist/failure-evidence.d.ts +2 -0
- package/dist/failure-evidence.js +14 -3
- package/dist/index.d.ts +6 -2
- package/dist/index.js +16 -9
- package/dist/input.d.ts +11 -0
- package/dist/input.js +48 -5
- package/dist/presentation.js +1 -1
- package/dist/provider.d.ts +5 -0
- package/dist/provider.js +1 -0
- package/dist/stage-timing.d.ts +4 -0
- package/dist/stage-timing.js +27 -5
- package/locale/en.json +2 -2
- package/locale/zh.json +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
**`@ai-workbench/remote-agent@0.8.
|
|
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.
|
|
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.
|
|
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
|
-
在官方 **设置 → 内置插件 → 远程助手参数**
|
|
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
|
-
|
|
56
|
+
提供方的中文文案保留已有英文名称及既有locale机制。提供方名称不附状态后缀,由原注册replace更新,不自造刷新或客户端品牌分支。
|
|
56
57
|
|
|
57
58
|
## 安装产物
|
|
58
59
|
|
|
@@ -60,9 +61,13 @@ Codex单次模式首版仅文本,不转接中途审批;图片或系统/开
|
|
|
60
61
|
|
|
61
62
|
## 验证范围
|
|
62
63
|
|
|
63
|
-
|
|
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
|
|
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.
|
|
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
|
|
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,17 @@ 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
|
-
|
|
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,3 +1,5 @@
|
|
|
1
|
+
import { employeeExecutionInfo, employeeMcpSummary } 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";
|
|
@@ -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
|
-
|
|
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.
|
|
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
|
|
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();
|
|
@@ -629,8 +648,6 @@ export function acpNeed(error) {
|
|
|
629
648
|
return new NeedError('ACP_FAILED', '远端 ACP 请求失败,根因未查明;未切换到新会话', `有效的远端 ACP 连接;RPC 错误码 ${error.code}`);
|
|
630
649
|
return new NeedError('ACP_FAILED', '远端 ACP 请求失败或连接中断,根因未查明;未切换到新会话', '有效的远端 ACP 连接');
|
|
631
650
|
}
|
|
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
651
|
function record(value) { return value !== null && typeof value === 'object' && !Array.isArray(value) ? value : undefined; }
|
|
634
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']);
|
|
635
653
|
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"]);
|
package/dist/approval-label.d.ts
CHANGED
|
@@ -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;
|
package/dist/approval-label.js
CHANGED
|
@@ -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 → 远程助手参数.
|
|
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;
|
|
@@ -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 === '
|
|
404
|
-
: observation.kind === 'session' &&
|
|
405
|
-
: observation.kind === '
|
|
406
|
-
: observation.kind === '
|
|
407
|
-
:
|
|
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) {
|
package/dist/employee-exec.d.ts
CHANGED
|
@@ -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;
|
package/dist/employee-exec.js
CHANGED
|
@@ -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
|
-
|
|
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
|
+
}
|
|
@@ -210,7 +210,7 @@ export function createEmployeeProvider(get, options, agent) {
|
|
|
210
210
|
const scopedIo = { run: (command, signal) => io.run(command, signal), attach: async (command) => {
|
|
211
211
|
const observe = closeDiagnostics.capture();
|
|
212
212
|
const child = await io.attach(command);
|
|
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)
|
|
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)
|
|
214
214
|
void candidate.dispose(fail('EMPLOYEE_EXEC_UNKNOWN'), { layer: 'runtime', initiator: 'provider.child-close', reason: 'EMPLOYEE_EXEC_UNKNOWN', action: 'dispose' }).catch(() => { }); });
|
|
215
215
|
return child;
|
|
216
216
|
} };
|
|
@@ -255,7 +255,8 @@ export function createEmployeeProvider(get, options, agent) {
|
|
|
255
255
|
}
|
|
256
256
|
const disposers = [];
|
|
257
257
|
const execution = new EmployeeExecution({ closeDiagnostics, bindingId: admitted.bindingId, acpMaxMessageBytes: request.acpMaxMessageBytes, validate, transport: signal => createEmployeeSshTransport(ssh, signal) });
|
|
258
|
-
const runtime = { ...host, closeDiagnostics, cwd: admitted.cwd, details, connection: { ...connection }, binding: { bindingId: admitted.bindingId, workspaceId: admitted.workspaceId, revision: state.revision }, saveCheckpoint: undefined, recoveredSession: undefined, employeeExecutionMeta: { version: 1, bindingId: admitted.bindingId, workspaceId: admitted.workspaceId, cwd: admitted.cwd, acpMaxMessageBytes: request.acpMaxMessageBytes }, 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;
|
|
259
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' }) => {
|
|
260
261
|
closeDiagnostics.record({ ...cause, changes: cause.changes ?? [{ field: 'provider.ended', before: entry.ended, after: true }] });
|
|
261
262
|
if (entry.closing)
|
|
@@ -310,13 +311,13 @@ export function createEmployeeProvider(get, options, agent) {
|
|
|
310
311
|
runtime.employeeExecution = async (params, signal) => {
|
|
311
312
|
try {
|
|
312
313
|
const result = await execution.handle(params, signal);
|
|
313
|
-
if (result.closed)
|
|
314
|
+
if (result.closed && !execution.remoteClose)
|
|
314
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' }));
|
|
315
316
|
return { ...result };
|
|
316
317
|
}
|
|
317
318
|
catch (error) {
|
|
318
319
|
// EmployeeExecution reserves ABORTED for an unconsumed read wait. Lost responses are UNKNOWN.
|
|
319
|
-
if (!(params && typeof params === 'object' && 'operation' in params && params.operation === 'read' && errorCode(error) === 'ABORTED'))
|
|
320
|
+
if (!execution.remoteClose && !(params && typeof params === 'object' && 'operation' in params && params.operation === 'read' && errorCode(error) === 'ABORTED'))
|
|
320
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' }));
|
|
321
322
|
throw withFirstFailureEvidence(error, closeDiagnostics.first?.reason, errorCode(error));
|
|
322
323
|
}
|
|
@@ -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;
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/** Approved protocol codes only; remote exception text is never a reason. */
|
|
2
|
+
export const employeeAdapterCodes = new Set([
|
|
3
|
+
"AW_EMPLOYEE_ADDITIONAL_DIRECTORIES_UNSUPPORTED",
|
|
4
|
+
"AW_EMPLOYEE_SLASH_COMMAND_UNSUPPORTED",
|
|
5
|
+
"AW_EMPLOYEE_HOST_FILE_REPORT_UNSUPPORTED",
|
|
6
|
+
"AW_EMPLOYEE_CONFIG_DISCOVERY_OVERRIDE_UNSUPPORTED",
|
|
7
|
+
"AW_EMPLOYEE_CONFIG_ADMISSION_INVALID",
|
|
8
|
+
"AW_EMPLOYEE_CONFIG_ADMISSION_FAILED",
|
|
9
|
+
"AW_EMPLOYEE_HOST_PROJECT_CONFIG_UNSUPPORTED",
|
|
10
|
+
"AW_EMPLOYEE_EXECUTOR_MCP_UNSUPPORTED",
|
|
11
|
+
"AW_EMPLOYEE_EXECUTOR_CONFIG_PROTOCOL_INVALID",
|
|
12
|
+
"AW_EMPLOYEE_EXECUTOR_CONFIG_READ_FAILED",
|
|
13
|
+
"AW_EMPLOYEE_AGENT_CONFIG_UNAVAILABLE",
|
|
14
|
+
"AW_EMPLOYEE_BINDING_INVALID",
|
|
15
|
+
"AW_EMPLOYEE_BINDING_MISMATCH",
|
|
16
|
+
"AW_EMPLOYEE_CAPABILITY_REQUIRED",
|
|
17
|
+
"AW_EMPLOYEE_CLOSE_FAILED",
|
|
18
|
+
"AW_EMPLOYEE_CLOSE_RESPONSE_INVALID",
|
|
19
|
+
"AW_EMPLOYEE_COLD_RESUME_UNSUPPORTED",
|
|
20
|
+
"AW_EMPLOYEE_CWD_MISMATCH",
|
|
21
|
+
"AW_EMPLOYEE_ENVIRONMENT_REGISTRATION_FAILED",
|
|
22
|
+
"AW_EMPLOYEE_ENVIRONMENT_REQUIRED",
|
|
23
|
+
"AW_EMPLOYEE_EXECUTION_CLOSED",
|
|
24
|
+
"AW_EMPLOYEE_EXECUTION_ID_REQUIRED",
|
|
25
|
+
"AW_EMPLOYEE_HOST_AUXILIARY_CONFIG_UNSUPPORTED",
|
|
26
|
+
"AW_EMPLOYEE_HOST_AUXILIARY_UNSUPPORTED",
|
|
27
|
+
"AW_EMPLOYEE_HOST_FALLBACK_FORBIDDEN",
|
|
28
|
+
"AW_EMPLOYEE_MCP_UNSUPPORTED",
|
|
29
|
+
"AW_EMPLOYEE_MESSAGE_INVALID",
|
|
30
|
+
"AW_EMPLOYEE_MESSAGE_TOO_LARGE",
|
|
31
|
+
"AW_EMPLOYEE_OPEN_RESPONSE_INVALID",
|
|
32
|
+
"AW_EMPLOYEE_READ_MESSAGE_INVALID",
|
|
33
|
+
"AW_EMPLOYEE_READ_RESPONSE_INVALID",
|
|
34
|
+
"AW_EMPLOYEE_RELAY_ADDRESS_INVALID",
|
|
35
|
+
"AW_EMPLOYEE_RELAY_FAILED",
|
|
36
|
+
"AW_EMPLOYEE_REQUEST_INVALID",
|
|
37
|
+
"AW_EMPLOYEE_SESSION_UNKNOWN",
|
|
38
|
+
"AW_EMPLOYEE_WIRE_FAILED",
|
|
39
|
+
"AW_EMPLOYEE_WRITE_RESPONSE_INVALID"
|
|
40
|
+
]);
|
|
41
|
+
export const employeeOpenCauseCodes = new Set([
|
|
42
|
+
"AW_EMPLOYEE_BINARY_UNAVAILABLE",
|
|
43
|
+
"AW_EMPLOYEE_BINARY_HASH_FAILED",
|
|
44
|
+
"AW_EMPLOYEE_BINARY_HASH_MISMATCH",
|
|
45
|
+
"AW_EMPLOYEE_BINARY_VERSION_FAILED",
|
|
46
|
+
"AW_EMPLOYEE_BINARY_VERSION_MISMATCH",
|
|
47
|
+
"EMPLOYEE_CONTEXT_CHANGED",
|
|
48
|
+
"EMPLOYEE_ENDPOINT_INVALID",
|
|
49
|
+
"EMPLOYEE_BINARY_REQUIRED",
|
|
50
|
+
"EMPLOYEE_EXECUTION_UNSUPPORTED",
|
|
51
|
+
"EMPLOYEE_EXEC_PROTOCOL_INVALID",
|
|
52
|
+
"EMPLOYEE_EXEC_IDENTITY_MISMATCH",
|
|
53
|
+
"EMPLOYEE_EXEC_CLOSED",
|
|
54
|
+
"EMPLOYEE_EXEC_UNKNOWN",
|
|
55
|
+
"EMPLOYEE_EXEC_ALREADY_OPEN",
|
|
56
|
+
"EMPLOYEE_EXEC_READ_BUSY",
|
|
57
|
+
"ACP_MESSAGE_TOO_LARGE",
|
|
58
|
+
"ABORTED",
|
|
59
|
+
"SETTINGS_REQUIRED"
|
|
60
|
+
]);
|
|
61
|
+
export function trustedEmployeeReason(value) { return typeof value === 'string' && (employeeAdapterCodes.has(value) || employeeOpenCauseCodes.has(value)) ? value : undefined; }
|
|
@@ -6,6 +6,8 @@ export declare function closeErrorCodes(error: unknown, fallback?: string): stri
|
|
|
6
6
|
export declare function closeFailure(code: string, message: string, requires: string, codes: readonly string[]): NeedError;
|
|
7
7
|
/** Saving succeeded: retain the original trusted error type, code and presentation. */
|
|
8
8
|
export declare function withFirstFailureEvidence<T>(primary: T, firstReason: unknown, primaryCode: string): T;
|
|
9
|
+
/** A negotiated close reason is independent evidence, never a replacement error or new gate. */
|
|
10
|
+
export declare function withRemoteFailureEvidence<T>(primary: T, remoteReason: unknown, primaryCode: string): T;
|
|
9
11
|
/** A known turn failure is final even while its original resource is still closing. */
|
|
10
12
|
export declare function withPendingCleanupEvidence<T>(primary: T, firstReason: unknown, primaryCode: string): T;
|
|
11
13
|
/** Preserve causal roles without copying arbitrary exception text into employee output. */
|
package/dist/failure-evidence.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { NeedError } from "./remote.js";
|
|
2
|
+
import { trustedEmployeeReason } from "./employee-reasons.js";
|
|
2
3
|
/** Only typed codes, never exception messages, causes, stderr or RPC data. */
|
|
3
4
|
export function closeCode(error, fallback = 'CLOSE_REASON_UNKNOWN') {
|
|
4
5
|
return error instanceof NeedError && /^[A-Z][A-Z0-9_]*$/.test(error.code) ? error.code : fallback;
|
|
@@ -58,7 +59,16 @@ function retainedEvidence(error) {
|
|
|
58
59
|
export function withFirstFailureEvidence(primary, firstReason, primaryCode) {
|
|
59
60
|
if (primary && typeof primary === 'object' && safeCode(firstReason) && safeCode(primaryCode)) {
|
|
60
61
|
const previous = retainedEvidence(primary);
|
|
61
|
-
evidence.set(primary, Object.freeze({ first: previous?.first ?? firstReason, original: previous?.original ?? Object.freeze([primaryCode]), diagnostic: previous?.diagnostic ?? Object.freeze([]), cleanup: previous?.cleanup ?? Object.freeze([]) }));
|
|
62
|
+
evidence.set(primary, Object.freeze({ ...previous, first: previous?.first ?? firstReason, original: previous?.original ?? Object.freeze([primaryCode]), diagnostic: previous?.diagnostic ?? Object.freeze([]), cleanup: previous?.cleanup ?? Object.freeze([]) }));
|
|
63
|
+
}
|
|
64
|
+
return primary;
|
|
65
|
+
}
|
|
66
|
+
/** A negotiated close reason is independent evidence, never a replacement error or new gate. */
|
|
67
|
+
export function withRemoteFailureEvidence(primary, remoteReason, primaryCode) {
|
|
68
|
+
const remote = trustedEmployeeReason(remoteReason);
|
|
69
|
+
if (primary && typeof primary === 'object' && remote) {
|
|
70
|
+
const previous = retainedEvidence(primary);
|
|
71
|
+
evidence.set(primary, Object.freeze({ ...previous, remote: previous?.remote ?? remote, original: previous?.original ?? Object.freeze([safeCode(primaryCode) ? primaryCode : 'TURN_FAILED']), diagnostic: previous?.diagnostic ?? Object.freeze([]), cleanup: previous?.cleanup ?? Object.freeze([]) }));
|
|
62
72
|
}
|
|
63
73
|
return primary;
|
|
64
74
|
}
|
|
@@ -75,7 +85,8 @@ export function withFailureEvidence(primary, secondary, kind, firstReason, prima
|
|
|
75
85
|
const previous = retainedEvidence(primary), first = previous?.first ?? (safeCode(firstReason) ? firstReason : undefined);
|
|
76
86
|
const original = previous?.original ?? (safeCode(primaryCode) ? [primaryCode] : closeErrorCodes(primary, 'TURN_FAILED'));
|
|
77
87
|
const secondaryFacts = secondary.map(retainedEvidence), added = [...new Set(secondary.flatMap((error, index) => secondaryFacts[index] ? secondaryFacts[index].original.filter(code => !original.includes(code)) : closeErrorCodes(error, kind === 'diagnostic' ? 'DIAGNOSTIC_WRITE_FAILED' : 'REMOTE_CLEANUP_UNCONFIRMED')))];
|
|
78
|
-
const
|
|
88
|
+
const remote = previous?.remote ?? secondaryFacts.find(f => f?.remote)?.remote;
|
|
89
|
+
const facts = Object.freeze({ ...(first ? { first } : {}), ...(remote ? { remote } : {}), original: Object.freeze(original), diagnostic: Object.freeze([...new Set([...(previous?.diagnostic ?? []), ...secondaryFacts.flatMap(f => f?.diagnostic ?? []), ...(kind === 'diagnostic' ? added : [])])]), cleanup: Object.freeze([...new Set([...(previous?.cleanup ?? []), ...secondaryFacts.flatMap(f => f?.cleanup ?? []), ...(kind === 'cleanup' ? added : [])])]) });
|
|
79
90
|
const code = kind === 'cleanup' ? 'REMOTE_CLEANUP_UNCONFIRMED' : !secondary.length ? primary instanceof NeedError ? primary.code : safeCode(primaryCode) ? primaryCode : 'TURN_FAILED' : primary instanceof NeedError && facts.diagnostic.length > 0 && facts.diagnostic.every(code => code === primary.code) && !facts.cleanup.length && !previous ? primary.code : 'CONVERSATION_DETAILS_FAILED';
|
|
80
91
|
const failure = Object.assign(new NeedError(code, '本轮已停止;运行原因与诊断或清理结果分别保留。', '核对原始原因及未确认的保存/清理结果'), { cause: new AggregateError([primary, ...(safeCode(primaryCode) && !(primary instanceof NeedError) ? [new NeedError(primaryCode, '运行错误代码', '可信调用点提供的固定代码')] : []), ...secondary, ...(first ? [new NeedError(first, '首次观察关闭代码', '不是已证明根因')] : [])], 'TURN_FAILURE_EVIDENCE') });
|
|
81
92
|
evidence.set(failure, facts);
|
|
@@ -87,5 +98,5 @@ export function failureEvidenceSummary(error) {
|
|
|
87
98
|
const facts = retainedEvidence(error);
|
|
88
99
|
if (!facts)
|
|
89
100
|
return '';
|
|
90
|
-
return '技术详情:' + [...(facts.first ? ['首次观察关闭 ' + facts.first] : []), '运行原因 ' + facts.original.join('、'), ...(facts.diagnostic.length ? ['详情保存未确认 ' + facts.diagnostic.join('、')] : []), ...(facts.cleanup.length ? ['清理未确认 ' + facts.cleanup.join('、')] : facts.cleanupPending ? ['正在确认主机清理,原资源清理结果尚待确认'] : [])].join(';');
|
|
101
|
+
return '技术详情:' + [...(facts.first ? ['首次观察关闭 ' + facts.first] : []), '运行原因 ' + facts.original.join('、'), ...(facts.remote ? ['远端关闭原因 ' + facts.remote] : []), ...(facts.diagnostic.length ? ['详情保存未确认 ' + facts.diagnostic.join('、')] : []), ...(facts.cleanup.length ? ['清理未确认 ' + facts.cleanup.join('、')] : facts.cleanupPending ? ['正在确认主机清理,原资源清理结果尚待确认'] : [])].join(';');
|
|
91
102
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -5,7 +5,7 @@ import { LlmAdapter, type GenerateOptions, type StreamChunk } from '@deepseek-ai
|
|
|
5
5
|
import { type AcpHooks } from './acp.ts';
|
|
6
6
|
import { type RuntimeProvider } from './provider.ts';
|
|
7
7
|
import { type AgentDefinition } from './agents.ts';
|
|
8
|
-
import { type ImageReader } from './input.ts';
|
|
8
|
+
import { type ImageReader, type HostWorkspacePresentation } from './input.ts';
|
|
9
9
|
import type { LoginDiagnostic } from './login-diagnostic.ts';
|
|
10
10
|
import { type ModelDirectory, type HostModel } from './model-catalog.ts';
|
|
11
11
|
import { type PreflightDiagnostics } from './preflight-diagnostic.ts';
|
|
@@ -21,8 +21,10 @@ export declare const DEFAULT_CONFIG: Readonly<{
|
|
|
21
21
|
readonly oneShotSandbox: "workspace-write";
|
|
22
22
|
readonly oneShotApproval: "never";
|
|
23
23
|
readonly employeeShellKillGraceMs: 1000;
|
|
24
|
+
readonly employeeHttpMcpPolicy: "reject";
|
|
24
25
|
}>;
|
|
25
26
|
export declare const Config: Schema<Schemastery.ObjectS<NoInfer<{
|
|
27
|
+
employeeHttpMcpPolicy: Schema<"reject" | "ignore", "reject" | "ignore", "volatile-defined">;
|
|
26
28
|
mode: Schema<"session" | "one-shot", "session" | "one-shot", "volatile-defined">;
|
|
27
29
|
acpMaxMessageBytes: Schema<number, number, "volatile-defined">;
|
|
28
30
|
brokerMaxFrameBytes: Schema<number, number, "volatile-defined">;
|
|
@@ -31,6 +33,7 @@ export declare const Config: Schema<Schemastery.ObjectS<NoInfer<{
|
|
|
31
33
|
oneShotApproval: Schema<"never", "never", "volatile-defined">;
|
|
32
34
|
employeeShellKillGraceMs: Schema<number, number, "volatile-defined">;
|
|
33
35
|
}>>, Schemastery.ObjectT<NoInfer<{
|
|
36
|
+
employeeHttpMcpPolicy: Schema<"reject" | "ignore", "reject" | "ignore", "volatile-defined">;
|
|
34
37
|
mode: Schema<"session" | "one-shot", "session" | "one-shot", "volatile-defined">;
|
|
35
38
|
acpMaxMessageBytes: Schema<number, number, "volatile-defined">;
|
|
36
39
|
brokerMaxFrameBytes: Schema<number, number, "volatile-defined">;
|
|
@@ -47,6 +50,7 @@ export interface PluginConfig {
|
|
|
47
50
|
oneShotSandbox?: 'read-only' | 'workspace-write' | 'danger-full-access';
|
|
48
51
|
oneShotApproval?: 'never';
|
|
49
52
|
employeeShellKillGraceMs?: number;
|
|
53
|
+
employeeHttpMcpPolicy?: 'reject' | 'ignore';
|
|
50
54
|
}
|
|
51
55
|
type LiveConfig = {
|
|
52
56
|
[K in keyof PluginConfig]: PluginConfig[K] | {
|
|
@@ -150,6 +154,6 @@ export declare class RemoteAgentsAdapter extends LlmAdapter {
|
|
|
150
154
|
dispose(): Promise<void>;
|
|
151
155
|
}
|
|
152
156
|
export declare function apply(ctx: Context, live: LiveConfig): void;
|
|
153
|
-
type InvocationApproval = (request: Parameters<NonNullable<AcpHooks['approve']>>[0], signal: AbortSignal | undefined, options: GenerateOptions, context?: string) => ReturnType<NonNullable<AcpHooks['approve']>>;
|
|
157
|
+
type InvocationApproval = (request: Parameters<NonNullable<AcpHooks['approve']>>[0], signal: AbortSignal | undefined, options: GenerateOptions, context?: string, hostWorkspace?: HostWorkspacePresentation) => ReturnType<NonNullable<AcpHooks['approve']>>;
|
|
154
158
|
export declare function dshApproval(ctx: Context): InvocationApproval;
|
|
155
159
|
export {};
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { connectNeedMessage } from "./connect-need.js";
|
|
2
2
|
import { CloseDiagnostics, withFailureEvidence, withFirstFailureEvidence, failureEvidenceSummary, closeCode, closeErrorCodes, closeFailure } from "./close-diagnostics.js";
|
|
3
|
-
import { withPendingCleanupEvidence } from "./failure-evidence.js";
|
|
3
|
+
import { withPendingCleanupEvidence, withRemoteFailureEvidence } from "./failure-evidence.js";
|
|
4
4
|
import { runCatalog } from "./catalog-ssh.js";
|
|
5
5
|
import { createCatalogDiagnostics, CatalogDiagnosticsRemote } from "./catalog-diagnostics.js";
|
|
6
6
|
import { SessionId } from '@deepseek-ai/dsh-session';
|
|
@@ -17,7 +17,7 @@ import { StreamMapper } from "./mapping.js";
|
|
|
17
17
|
import { connectProvider } from "./provider.js";
|
|
18
18
|
import { brokerFactory } from "./broker.js";
|
|
19
19
|
import { AGENTS, CODEX_AGENT } from "./agents.js";
|
|
20
|
-
import { prepareInput, prepareContinuation } from "./input.js";
|
|
20
|
+
import { prepareInput, prepareContinuation, projectHostWorkspaceInput } from "./input.js";
|
|
21
21
|
import { createWorkspaceNoticeConsumer, isWorkspaceNoticeMessage } from "./workspace-notice.js";
|
|
22
22
|
import { NeedError } from "./remote.js";
|
|
23
23
|
import { employeeFailure } from "./presentation.js";
|
|
@@ -45,8 +45,9 @@ export const settingsNamespace = 'ai-workbench-codex';
|
|
|
45
45
|
// L can be installed/enabled after X; keeping X mounted lets it explain the prerequisite.
|
|
46
46
|
export const inject = ['llm'];
|
|
47
47
|
/** Owner-approved defaults; administrators may override through the existing settings. */
|
|
48
|
-
export const DEFAULT_CONFIG = Object.freeze({ mode: 'session', acpMaxMessageBytes: 4194304, brokerMaxFrameBytes: 2097152, brokerControlMaxBytes: 1048576, oneShotSandbox: 'workspace-write', oneShotApproval: 'never', employeeShellKillGraceMs: 1000 });
|
|
48
|
+
export const DEFAULT_CONFIG = Object.freeze({ mode: 'session', acpMaxMessageBytes: 4194304, brokerMaxFrameBytes: 2097152, brokerControlMaxBytes: 1048576, oneShotSandbox: 'workspace-write', oneShotApproval: 'never', employeeShellKillGraceMs: 1000, employeeHttpMcpPolicy: 'reject' });
|
|
49
49
|
export const Config = Schema.object({
|
|
50
|
+
employeeHttpMcpPolicy: Schema.union([Schema.const('reject').i18n({ zh: '拒绝本机 HTTP MCP(默认)', en: '拒绝本机 HTTP MCP(默认)' }), Schema.const('ignore').i18n({ zh: '忽略本机 HTTP MCP 后继续', en: '忽略本机 HTTP MCP 后继续' })]).i18n({ zh: '本机 HTTP MCP 策略;默认拒绝;忽略仅剔除会话投影,不改员工配置。新连接生效,stdio 始终按原生规则忽略并说明名称', en: '本机 HTTP MCP 策略;默认拒绝;忽略仅剔除会话投影,不改员工配置。新连接生效,stdio 始终按原生规则忽略并说明名称' }).default(DEFAULT_CONFIG.employeeHttpMcpPolicy).volatile(),
|
|
50
51
|
mode: Schema.union([Schema.const('session').i18n({ en: 'Session', zh: '会话模式' }), Schema.const('one-shot').i18n({ en: 'One-shot', zh: '单次模式' })]).i18n({ en: 'Run mode; start a new session after switching modes', zh: 'Codex运行模式;Claude固定会话模式;切换后请新建会话' }).default(DEFAULT_CONFIG.mode).volatile(),
|
|
51
52
|
acpMaxMessageBytes: Schema.number().min(1).step(1).i18n({ en: 'Session message limit; default 4 MiB, adjustable by administrators', zh: '会话消息容量;默认4 MiB,管理员可调整' }).default(DEFAULT_CONFIG.acpMaxMessageBytes).volatile(),
|
|
52
53
|
brokerMaxFrameBytes: Schema.number().min(1).step(1).i18n({ en: 'Host output chunk limit; default 2 MiB, adjustable by administrators', zh: '主机回传单段容量;默认2 MiB,管理员可调整' }).default(DEFAULT_CONFIG.brokerMaxFrameBytes).volatile(),
|
|
@@ -340,7 +341,7 @@ export class CodexRemoteAdapter extends LlmAdapter {
|
|
|
340
341
|
let turnFailure;
|
|
341
342
|
let employeeNoticeShown = false;
|
|
342
343
|
const information = [];
|
|
343
|
-
const recordInformation = () => { if (runtime?.details)
|
|
344
|
+
const recordInformation = () => { timing.summarize(); if (runtime?.details)
|
|
344
345
|
for (const entry of information.splice(0))
|
|
345
346
|
runtime.details.record(entry); };
|
|
346
347
|
const config = this.config();
|
|
@@ -386,7 +387,7 @@ export class CodexRemoteAdapter extends LlmAdapter {
|
|
|
386
387
|
const configuration = JSON.stringify([mode, config.acpMaxMessageBytes, config.brokerMaxFrameBytes, config.brokerControlMaxBytes, config.employeeShellKillGraceMs]);
|
|
387
388
|
if (live && live.configuration !== configuration)
|
|
388
389
|
throw new NeedError('SESSION_CONTEXT_MISMATCH', '会话运行设置已改变,请新建会话', '原会话运行设置');
|
|
389
|
-
Object.assign(request, { timing, acpMaxMessageBytes: config.acpMaxMessageBytes, resumeSessionId: previous?.sessionId, recoveryCheckpointId: previous?.recoveryCheckpointId, sessionId: options.sessionId, mode, cwd: previous?.cwd, identity: previous?.identity, bindingId: previous?.bindingId, workspaceId: previous?.workspaceId, agent: this.agent, retainProcess: !!key && mode === 'session', signal, brokerSettings: Object.freeze({ brokerMaxFrameBytes: config.brokerMaxFrameBytes, brokerControlMaxBytes: config.brokerControlMaxBytes }) });
|
|
390
|
+
Object.assign(request, { timing, employeeHttpMcpPolicy: config.employeeHttpMcpPolicy ?? DEFAULT_CONFIG.employeeHttpMcpPolicy, acpMaxMessageBytes: config.acpMaxMessageBytes, resumeSessionId: previous?.sessionId, recoveryCheckpointId: previous?.recoveryCheckpointId, sessionId: options.sessionId, mode, cwd: previous?.cwd, identity: previous?.identity, bindingId: previous?.bindingId, workspaceId: previous?.workspaceId, agent: this.agent, retainProcess: !!key && mode === 'session', signal, brokerSettings: Object.freeze({ brokerMaxFrameBytes: config.brokerMaxFrameBytes, brokerControlMaxBytes: config.brokerControlMaxBytes }) });
|
|
390
391
|
if (requireFreshSession) {
|
|
391
392
|
request.requireFreshSession = true;
|
|
392
393
|
request.resumeSessionId = undefined;
|
|
@@ -487,12 +488,17 @@ export class CodexRemoteAdapter extends LlmAdapter {
|
|
|
487
488
|
if (contextChanged)
|
|
488
489
|
input = await timing.run('input-prepare', () => prepareContinuation(options, input, this.readImage));
|
|
489
490
|
}
|
|
491
|
+
input = projectHostWorkspaceInput(input, runtime.hostWorkspace);
|
|
492
|
+
if (runtime.hostWorkspace && mode === 'session' && Buffer.byteLength(JSON.stringify({ prompt: input.prompt, systemPrompt: input.systemPrompt }), 'utf8') > config.acpMaxMessageBytes)
|
|
493
|
+
throw new NeedError('ACP_MESSAGE_TOO_LARGE', '本轮输入超过已设置的会话消息容量,未截断或发送', '管理员调整acpMaxMessageBytes');
|
|
490
494
|
const details = runtime.details;
|
|
491
495
|
if (!details)
|
|
492
496
|
throw new NeedError('TECHNICAL_DETAILS_UNAVAILABLE', '技术详情暂不可用,请更新远程主机连接组件。', 'L技术详情持久接收能力');
|
|
493
497
|
recordInformation();
|
|
494
498
|
timing.bind(detail => details.record(detail));
|
|
495
499
|
request.recordEarlyFailure = undefined;
|
|
500
|
+
if (runtime.hostWorkspace)
|
|
501
|
+
details.record({ kind: 'session', severity: 'info', code: 'HOST_WORKSPACE_CONTEXT', summary: '本轮模型工作区上下文及审批会话目录使用已确认的主机目录。', payload: { source: 'host', cwd: runtime.hostWorkspace.cwd } });
|
|
496
502
|
const mapper = new StreamMapper(mode === 'session' && !contextChanged ? (runtime.restartNotice ? undefined : live ? live.usageSnapshot : previous ? (previous.usageSnapshot ?? null) : undefined) : undefined, observation => details.record(observation));
|
|
497
503
|
activeMapper = mapper;
|
|
498
504
|
let id;
|
|
@@ -530,7 +536,7 @@ export class CodexRemoteAdapter extends LlmAdapter {
|
|
|
530
536
|
let done = false;
|
|
531
537
|
let failure;
|
|
532
538
|
const push = (chunks) => { queue.push(...chunks); wake?.(); wake = undefined; };
|
|
533
|
-
const hooks = { closeDiagnostics: runtime.closeDiagnostics, broker: runtime.broker, employeeExecution: runtime.employeeExecution, timing, approve: async (permission, approvalSignal) => {
|
|
539
|
+
const hooks = { onEmployeeInfo: summary => push(mapper.text(summary + '\n\n')), closeDiagnostics: runtime.closeDiagnostics, broker: runtime.broker, employeeExecution: runtime.employeeExecution, timing, approve: async (permission, approvalSignal) => {
|
|
534
540
|
const execution = runtime.execution;
|
|
535
541
|
const toolName = permission.toolCall.name ?? permission.toolCall._meta?.claudeCode?.toolName;
|
|
536
542
|
if (execution) {
|
|
@@ -541,7 +547,7 @@ export class CodexRemoteAdapter extends LlmAdapter {
|
|
|
541
547
|
}
|
|
542
548
|
await execution.validate(approvalSignal);
|
|
543
549
|
}
|
|
544
|
-
const answer = await this.approve?.(permission, approvalSignal, options, execution && toolName?.startsWith('mcp__employee__') ? 'employee-local' : undefined) ?? 'unavailable';
|
|
550
|
+
const answer = await this.approve?.(permission, approvalSignal, options, execution && toolName?.startsWith('mcp__employee__') ? 'employee-local' : undefined, runtime.hostWorkspace) ?? 'unavailable';
|
|
545
551
|
if (execution && answer === 'allowed-once')
|
|
546
552
|
await execution.validate(approvalSignal);
|
|
547
553
|
return answer;
|
|
@@ -704,6 +710,7 @@ export class CodexRemoteAdapter extends LlmAdapter {
|
|
|
704
710
|
runtime?.closeDiagnostics?.record({ layer: 'adapter', initiator: 'adapter.turn-failure', reason: closeCode(error, signal.aborted ? 'ABORTED' : 'TURN_FAILED'), action: 'failure' });
|
|
705
711
|
recordInformation();
|
|
706
712
|
failureCode = error instanceof NeedError || error instanceof ModelConfigurationError ? error.code : error instanceof LlmError ? error.code : 'TURN_PREPARATION_FAILED';
|
|
713
|
+
withRemoteFailureEvidence(error, runtime?.employeeRemoteCloseReason?.(), failureCode);
|
|
707
714
|
if (live && session && session.promptDispatchCount > promptBaseline)
|
|
708
715
|
live.failure = { cause: error };
|
|
709
716
|
const reporting = [];
|
|
@@ -969,12 +976,12 @@ export function apply(ctx, live) {
|
|
|
969
976
|
ctx.effect(() => () => { modelDirectory.dispose(); return adapter.dispose(); });
|
|
970
977
|
}
|
|
971
978
|
export function dshApproval(ctx) {
|
|
972
|
-
return async (request, signal, options, context) => {
|
|
979
|
+
return async (request, signal, options, context, hostWorkspace) => {
|
|
973
980
|
const agent = options.sessionId ? ctx.get('agents')?.get(options.sessionId) : undefined;
|
|
974
981
|
const approval = ctx.get('approval');
|
|
975
982
|
if (!agent || !approval)
|
|
976
983
|
return 'unavailable';
|
|
977
|
-
const label = approvalLabel(request, context === 'employee-local' ? 'local' : 'host');
|
|
984
|
+
const label = approvalLabel(request, context === 'employee-local' ? 'local' : 'host', hostWorkspace);
|
|
978
985
|
if (!label)
|
|
979
986
|
return 'unavailable';
|
|
980
987
|
return approval.request({ agent, ...label, signal });
|
package/dist/input.d.ts
CHANGED
|
@@ -12,6 +12,13 @@ export interface HistoryTransfer {
|
|
|
12
12
|
omittedReasoningBlocks: number;
|
|
13
13
|
reasoningOmissionReason: 'dsh-internal-reasoning';
|
|
14
14
|
}
|
|
15
|
+
export interface HostWorkspacePresentation {
|
|
16
|
+
readonly cwd: string;
|
|
17
|
+
}
|
|
18
|
+
type RuntimeSection = {
|
|
19
|
+
name: string;
|
|
20
|
+
text: string;
|
|
21
|
+
};
|
|
15
22
|
export interface PreparedInput {
|
|
16
23
|
prompt: AcpContentBlock[];
|
|
17
24
|
users: {
|
|
@@ -20,8 +27,12 @@ export interface PreparedInput {
|
|
|
20
27
|
}[];
|
|
21
28
|
systemPrompt?: string;
|
|
22
29
|
historyTransfer?: HistoryTransfer;
|
|
30
|
+
runtimeSnapshots?: ReadonlyMap<AcpContentBlock, readonly RuntimeSection[]>;
|
|
23
31
|
}
|
|
32
|
+
/** Invocation-only model context; never modifies the native session/history. */
|
|
33
|
+
export declare function projectHostWorkspaceInput(input: PreparedInput, host?: HostWorkspacePresentation): PreparedInput;
|
|
24
34
|
/** Role-labelled public history only; never serialize source/replay/tool internals. */
|
|
25
35
|
export declare function prepareContinuation(options: GenerateOptions, input: PreparedInput, readImage?: ImageReader): Promise<PreparedInput>;
|
|
26
36
|
/** The attachment service supplies verified bytes; client paths never cross SSH. */
|
|
27
37
|
export declare function prepareInput(options: GenerateOptions, readImage?: ImageReader): Promise<PreparedInput>;
|
|
38
|
+
export {};
|
package/dist/input.js
CHANGED
|
@@ -1,5 +1,37 @@
|
|
|
1
1
|
import { NeedError } from "./remote.js";
|
|
2
2
|
import { isWorkspaceNoticeMessage, withoutWorkspaceNotice } from "./workspace-notice.js";
|
|
3
|
+
function runtimeSections(message) {
|
|
4
|
+
const source = message.source;
|
|
5
|
+
if (message.role !== 'user' || source?.kind !== 'runtime-context' || source.form !== 'snapshot' || !Array.isArray(source.sections) || message.content.length !== 1 || message.content[0].type !== 'text')
|
|
6
|
+
return;
|
|
7
|
+
if (!source.sections.every(s => s && typeof s.name === 'string' && typeof s.text === 'string'))
|
|
8
|
+
return;
|
|
9
|
+
const sections = source.sections, body = sections.map(s => s.text).join('\n\n');
|
|
10
|
+
// Only official, structurally attributed snapshot text is projected. Employee
|
|
11
|
+
// text and opaque contributions are never searched for or rewritten as paths.
|
|
12
|
+
if (body && message.content[0].text.endsWith('\n\n' + body))
|
|
13
|
+
return sections;
|
|
14
|
+
}
|
|
15
|
+
/** Invocation-only model context; never modifies the native session/history. */
|
|
16
|
+
export function projectHostWorkspaceInput(input, host) {
|
|
17
|
+
if (!host)
|
|
18
|
+
return input;
|
|
19
|
+
const context = `当前会话使用远程主机上的工作区:${JSON.stringify(host.cwd)}。文件和命令在该主机目录执行,受主机账号的操作系统权限约束;本机 DSH 文件沙箱的工作区路径不适用于这些远程操作。`;
|
|
20
|
+
const mapped = new Map();
|
|
21
|
+
for (const [block, sections] of input.runtimeSnapshots ?? []) {
|
|
22
|
+
if (block.type !== 'text' || !sections.some(s => s.name === 'sandbox:policy'))
|
|
23
|
+
continue;
|
|
24
|
+
const body = sections.map(s => s.text).join('\n\n');
|
|
25
|
+
mapped.set(block, { ...block, text: block.text.slice(0, -body.length) + sections.map(s => s.name === 'sandbox:policy' ? context : s.text).join('\n\n') });
|
|
26
|
+
}
|
|
27
|
+
const project = (blocks) => blocks.map(block => mapped.get(block) ?? block);
|
|
28
|
+
const prompt = project(input.prompt);
|
|
29
|
+
// Confirm this invocation after all current and historical text. A projected
|
|
30
|
+
// historical snapshot must not suppress host facts for an opaque current one;
|
|
31
|
+
// warm steps also need no new native snapshot to retain their host location.
|
|
32
|
+
prompt.push({ type: 'text', text: context });
|
|
33
|
+
return { ...input, prompt, users: input.users.map(user => ({ ...user, prompt: project(user.prompt) })) };
|
|
34
|
+
}
|
|
3
35
|
async function publicBlock(block, options, readImage, history = false) {
|
|
4
36
|
options.signal?.throwIfAborted();
|
|
5
37
|
if (block.type === 'text')
|
|
@@ -26,6 +58,7 @@ export async function prepareContinuation(options, input, readImage) {
|
|
|
26
58
|
throw new NeedError('HISTORY_CONTENT_UNSUPPORTED', `此前含有无法可靠承接的历史角色:${message.role};本轮未新建或发送`, '完整公开的用户及助手历史;工具记录的公开内容映射');
|
|
27
59
|
const after = messages.findLastIndex(message => message.role === 'assistant');
|
|
28
60
|
const prompt = [];
|
|
61
|
+
const runtimeSnapshots = new Map(input.runtimeSnapshots);
|
|
29
62
|
const historyTransfer = { scope: 'visible-user-assistant-text-and-images', textBlocks: 0, imageBlocks: 0, offloadedImagePlaceholders: 0, omittedReasoningBlocks: 0, reasoningOmissionReason: 'dsh-internal-reasoning' };
|
|
30
63
|
for (const message of messages.slice(0, after + 1)) {
|
|
31
64
|
if (message.role === 'system' || message.role === 'developer')
|
|
@@ -33,13 +66,17 @@ export async function prepareContinuation(options, input, readImage) {
|
|
|
33
66
|
if (message.role !== 'user' && message.role !== 'assistant')
|
|
34
67
|
throw new NeedError('HISTORY_CONTENT_UNSUPPORTED', `此前含有无法可靠承接的历史角色:${message.role};本轮未新建或发送`, '完整公开的用户及助手历史;工具记录的公开内容映射');
|
|
35
68
|
prompt.push({ type: 'text', text: `此前可见对话(历史角色:${message.role};仅为历史记录):\n` });
|
|
69
|
+
const sections = runtimeSections(message);
|
|
36
70
|
for (const block of message.content) {
|
|
37
71
|
// DSH explicitly distinguishes internal thinking from visible user text.
|
|
38
72
|
if (block.type === 'reasoning') {
|
|
39
73
|
historyTransfer.omittedReasoningBlocks++;
|
|
40
74
|
continue;
|
|
41
75
|
}
|
|
42
|
-
|
|
76
|
+
const prepared = await publicBlock(block, options, readImage, true);
|
|
77
|
+
prompt.push(prepared);
|
|
78
|
+
if (sections)
|
|
79
|
+
runtimeSnapshots.set(prepared, sections);
|
|
43
80
|
if (block.type === 'text')
|
|
44
81
|
historyTransfer.textBlocks++;
|
|
45
82
|
else if (block.type === 'image') {
|
|
@@ -52,7 +89,7 @@ export async function prepareContinuation(options, input, readImage) {
|
|
|
52
89
|
prompt.push({ type: 'text', text: `\n(历史角色:${message.role} 的记录结束)\n` });
|
|
53
90
|
}
|
|
54
91
|
prompt.push({ type: 'text', text: '此前可见对话结束。以下为当前用户输入:\n' }, ...input.prompt);
|
|
55
|
-
return { ...input, prompt, historyTransfer };
|
|
92
|
+
return { ...input, prompt, historyTransfer, runtimeSnapshots };
|
|
56
93
|
}
|
|
57
94
|
/** The attachment service supplies verified bytes; client paths never cross SSH. */
|
|
58
95
|
export async function prepareInput(options, readImage) {
|
|
@@ -78,13 +115,19 @@ export async function prepareInput(options, readImage) {
|
|
|
78
115
|
throw new NeedError('INPUT_REQUIRED', '我需要本轮用户输入', '用户输入');
|
|
79
116
|
const prompt = [];
|
|
80
117
|
const preparedUsers = [];
|
|
118
|
+
const runtimeSnapshots = new Map();
|
|
81
119
|
for (const message of users) {
|
|
82
120
|
const before = prompt.length;
|
|
83
|
-
|
|
84
|
-
|
|
121
|
+
const sections = runtimeSections(message);
|
|
122
|
+
for (const block of message.content) {
|
|
123
|
+
const prepared = await publicBlock(block, options, readImage);
|
|
124
|
+
prompt.push(prepared);
|
|
125
|
+
if (sections)
|
|
126
|
+
runtimeSnapshots.set(prepared, sections);
|
|
127
|
+
}
|
|
85
128
|
preparedUsers.push({ id: message.id, prompt: prompt.slice(before) });
|
|
86
129
|
}
|
|
87
130
|
if (!prompt.length)
|
|
88
131
|
throw new NeedError('INPUT_REQUIRED', '我需要本轮用户输入', '用户输入');
|
|
89
|
-
return { prompt, users: preparedUsers, ...(context.length ? { systemPrompt: context.join('\n\n') } : {}) };
|
|
132
|
+
return { prompt, users: preparedUsers, runtimeSnapshots, ...(context.length ? { systemPrompt: context.join('\n\n') } : {}) };
|
|
90
133
|
}
|
package/dist/presentation.js
CHANGED
|
@@ -81,7 +81,7 @@ export function employeeFailure(code, brand = 'codex') {
|
|
|
81
81
|
if (code === 'AW_EMPLOYEE_HOST_PROJECT_CONFIG_UNSUPPORTED')
|
|
82
82
|
return '主机上存在与本机工作区路径重合的项目配置,当前版本无法安全应用,本轮没有执行,请联系管理员。';
|
|
83
83
|
if (code === 'AW_EMPLOYEE_EXECUTOR_MCP_UNSUPPORTED')
|
|
84
|
-
return '
|
|
84
|
+
return '本机 HTTP MCP 当前配置策略为拒绝,本轮没有继续执行;管理员可在远程助手参数中调整本机 HTTP MCP 策略,新建会话后生效。';
|
|
85
85
|
if (['AW_EMPLOYEE_CONFIG_ADMISSION_INVALID', 'AW_EMPLOYEE_CONFIG_ADMISSION_FAILED', 'AW_EMPLOYEE_EXECUTOR_CONFIG_PROTOCOL_INVALID', 'AW_EMPLOYEE_EXECUTOR_CONFIG_READ_FAILED'].includes(code))
|
|
86
86
|
return '无法核验这次本机工作区的助手配置,本轮没有继续执行,请联系管理员查看技术详情。';
|
|
87
87
|
if (code === 'AW_EMPLOYEE_MCP_UNSUPPORTED')
|
package/dist/provider.d.ts
CHANGED
|
@@ -16,6 +16,7 @@ import type { EmployeeProviderOptions } from './employee-provider.ts';
|
|
|
16
16
|
import { type WorkspaceSourceOptions, type SourceAdmissionStage } from './workspace-source.ts';
|
|
17
17
|
import type { LocalUserTurnAcceptance } from '@ai-workbench/connect/api';
|
|
18
18
|
import type { EmployeeRouting } from './employee-relay.ts';
|
|
19
|
+
import type { HostWorkspacePresentation } from './input.ts';
|
|
19
20
|
export interface EmployeeExecution {
|
|
20
21
|
hostCwd: string;
|
|
21
22
|
routing: EmployeeRouting;
|
|
@@ -29,6 +30,7 @@ export interface EmployeeExecution {
|
|
|
29
30
|
export interface Runtime {
|
|
30
31
|
identity: string;
|
|
31
32
|
cwd: string;
|
|
33
|
+
hostWorkspace?: Readonly<HostWorkspacePresentation>;
|
|
32
34
|
contextSignal?: AbortSignal;
|
|
33
35
|
execution?: EmployeeExecution;
|
|
34
36
|
acpTransport?: {
|
|
@@ -63,9 +65,11 @@ export interface Runtime {
|
|
|
63
65
|
workspaceId: string;
|
|
64
66
|
cwd: string;
|
|
65
67
|
acpMaxMessageBytes: number;
|
|
68
|
+
httpMcpPolicy?: 'reject' | 'ignore';
|
|
66
69
|
};
|
|
67
70
|
employeeExecution?: (params: unknown, signal?: AbortSignal) => Promise<Record<string, unknown>>;
|
|
68
71
|
employeePresentation?: string;
|
|
72
|
+
employeeRemoteCloseReason?: () => string | undefined;
|
|
69
73
|
closeDiagnostics?: CloseDiagnostics;
|
|
70
74
|
}
|
|
71
75
|
export interface RuntimeRequest extends Invocation {
|
|
@@ -83,6 +87,7 @@ export interface RuntimeRequest extends Invocation {
|
|
|
83
87
|
detailChannel?: DetailChannel;
|
|
84
88
|
validateContext?: (ignoreCancellation?: boolean) => Promise<void>;
|
|
85
89
|
verifyRecovery?: () => Promise<boolean>;
|
|
90
|
+
employeeHttpMcpPolicy?: 'reject' | 'ignore';
|
|
86
91
|
acpMaxMessageBytes?: number;
|
|
87
92
|
employeeScope?: string;
|
|
88
93
|
contextSha256?: string;
|
package/dist/provider.js
CHANGED
|
@@ -342,6 +342,7 @@ export function connectProvider(get, options = {}) {
|
|
|
342
342
|
}
|
|
343
343
|
if (host)
|
|
344
344
|
retainHostScope(runtime, host);
|
|
345
|
+
runtime.hostWorkspace = host && 'directory' in resolved ? Object.freeze({ cwd: resolved.directory.path }) : undefined;
|
|
345
346
|
// The transport can outlive a turn; presentation belongs only to this invocation.
|
|
346
347
|
runtime.workspaceNotice = workspaceNotice || undefined;
|
|
347
348
|
runtime.details = details ?? runtime.details;
|
package/dist/stage-timing.d.ts
CHANGED
|
@@ -22,6 +22,10 @@ export declare class StageTiming {
|
|
|
22
22
|
private readonly started;
|
|
23
23
|
private pending;
|
|
24
24
|
private sink?;
|
|
25
|
+
private confirmations;
|
|
26
|
+
private emit;
|
|
27
|
+
/** Presentation aggregation only: no duration threshold and no new execution gate. */
|
|
28
|
+
summarize(): void;
|
|
25
29
|
bind(sink: (detail: DetailObservation) => void): void;
|
|
26
30
|
run<T>(phase: Stage, operation: () => Promise<T>): Promise<T>;
|
|
27
31
|
}
|
package/dist/stage-timing.js
CHANGED
|
@@ -11,6 +11,19 @@ export class StageTiming {
|
|
|
11
11
|
started = performance.now();
|
|
12
12
|
pending = [];
|
|
13
13
|
sink;
|
|
14
|
+
confirmations = { count: 0, total: 0, min: Infinity, max: 0, elapsed: 0 };
|
|
15
|
+
emit(detail) { if (this.sink)
|
|
16
|
+
this.sink(detail);
|
|
17
|
+
else
|
|
18
|
+
this.pending.push(detail); }
|
|
19
|
+
/** Presentation aggregation only: no duration threshold and no new execution gate. */
|
|
20
|
+
summarize() {
|
|
21
|
+
const current = this.confirmations;
|
|
22
|
+
if (!current.count)
|
|
23
|
+
return;
|
|
24
|
+
this.confirmations = { count: 0, total: 0, min: Infinity, max: 0, elapsed: 0 };
|
|
25
|
+
this.emit({ kind: 'session', severity: 'info', code: 'REMOTE_STAGE_TIMING', summary: STAGES['connection-confirm'] + ':' + current.count + '次,共' + current.total.toFixed(1) + '毫秒,最大' + current.max.toFixed(1) + '毫秒', payload: { phase: 'connection-confirm', count: current.count, totalDurationMs: current.total, minDurationMs: current.min, maxDurationMs: current.max, elapsedMs: current.elapsed, outcome: 'ok', clock: 'client-monotonic' } });
|
|
26
|
+
}
|
|
14
27
|
bind(sink) {
|
|
15
28
|
this.sink = sink;
|
|
16
29
|
const pending = this.pending;
|
|
@@ -29,11 +42,20 @@ export class StageTiming {
|
|
|
29
42
|
finally {
|
|
30
43
|
const ended = performance.now();
|
|
31
44
|
const durationMs = ended - started, elapsedMs = ended - this.started;
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
45
|
+
if (phase === 'connection-confirm' && outcome === 'ok') {
|
|
46
|
+
const current = this.confirmations;
|
|
47
|
+
current.count++;
|
|
48
|
+
current.total += durationMs;
|
|
49
|
+
current.min = Math.min(current.min, durationMs);
|
|
50
|
+
current.max = Math.max(current.max, durationMs);
|
|
51
|
+
current.elapsed = elapsedMs;
|
|
52
|
+
}
|
|
53
|
+
else {
|
|
54
|
+
if (phase !== 'connection-confirm')
|
|
55
|
+
this.summarize();
|
|
56
|
+
const detail = { kind: 'session', severity: outcome === 'ok' ? 'info' : 'warning', code: 'REMOTE_STAGE_TIMING', summary: STAGES[phase] + ':' + durationMs.toFixed(1) + '毫秒', payload: { phase, durationMs, elapsedMs, outcome, clock: 'client-monotonic' } };
|
|
57
|
+
this.emit(detail);
|
|
58
|
+
}
|
|
37
59
|
}
|
|
38
60
|
}
|
|
39
61
|
}
|
package/locale/en.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"meta": {
|
|
3
3
|
"title": "Codex (Remote Host)",
|
|
4
|
-
"description": "Run Codex sessions through a remote host. Administrators: use Settings → Open configuration file to edit the existing plugin entry. See README for the
|
|
4
|
+
"description": "Run Codex sessions through a remote host. Administrators: use Settings → Open configuration file to edit the existing plugin entry. See README for the eight fields and approved defaults."
|
|
5
5
|
},
|
|
6
6
|
"configuration": {
|
|
7
|
-
"administratorNote": "Settings → Built-in plugins → 远程助手参数.
|
|
7
|
+
"administratorNote": "Settings → Built-in plugins → 远程助手参数. Eight named settings with defaults, scope and effects.",
|
|
8
8
|
"providerName": "Codex (Remote Host)"
|
|
9
9
|
}
|
|
10
10
|
}
|
package/locale/zh.json
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
"description": "通过远程主机运行Claude与Codex。首版不支持总是允许和歧义审批选项、已有Claude会话中立即生效的系统上下文变更;完整说明见README。参数从设置 → 内置插件 → 远程助手参数编辑。"
|
|
5
5
|
},
|
|
6
6
|
"configuration": {
|
|
7
|
-
"administratorNote": "管理员:设置 → 内置插件 →
|
|
7
|
+
"administratorNote": "管理员:设置 → 内置插件 → 远程助手参数。八项设置含默认值、范围、生效和后果说明。",
|
|
8
8
|
"providerName": "Codex(远程主机)"
|
|
9
9
|
}
|
|
10
10
|
}
|