@ai-workbench/connect 0.7.7 → 0.7.11

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.
Files changed (42) hide show
  1. package/README.md +62 -5
  2. package/client/index.cjs +33 -5
  3. package/helper/auth-status.cjs +6 -6
  4. package/helper/device-panel.cjs +4 -0
  5. package/helper/device-panel.css +1 -0
  6. package/helper/device-panel.html +1 -0
  7. package/helper/device-preload.cjs +5 -0
  8. package/helper/device-window.cjs +60 -0
  9. package/helper/remote-command.cjs +3 -2
  10. package/helper/remote-login.cjs +1 -1
  11. package/helper/window.cjs +8 -14
  12. package/lib/api.d.ts +98 -1
  13. package/lib/auth-diagnostics.d.ts +22 -0
  14. package/lib/auth-diagnostics.js +45 -0
  15. package/lib/auth-status.js +21 -8
  16. package/lib/components.d.ts +1 -1
  17. package/lib/components.js +72 -4
  18. package/lib/connect.js +93 -27
  19. package/lib/conversation-detail-settings.d.ts +32 -0
  20. package/lib/conversation-detail-settings.js +119 -0
  21. package/lib/conversation-details.d.ts +24 -0
  22. package/lib/conversation-details.js +382 -0
  23. package/lib/cordis.d.ts +3 -1
  24. package/lib/defaults.d.ts +4 -0
  25. package/lib/defaults.js +1 -0
  26. package/lib/index.d.ts +4 -0
  27. package/lib/index.js +23 -15
  28. package/lib/integration.js +21 -1
  29. package/lib/login-diagnostics.d.ts +26 -0
  30. package/lib/login-diagnostics.js +42 -0
  31. package/lib/login.d.ts +8 -4
  32. package/lib/login.js +16 -25
  33. package/lib/needs.js +8 -0
  34. package/lib/processes.js +34 -7
  35. package/lib/profile-runtime.js +51 -11
  36. package/lib/profile-settings.js +3 -3
  37. package/lib/ssh.js +37 -14
  38. package/lib/startup.d.ts +22 -1
  39. package/lib/startup.js +124 -33
  40. package/lib/workspace-bindings.d.ts +4 -1
  41. package/lib/workspace-bindings.js +51 -1
  42. package/package.json +1 -1
package/README.md CHANGED
@@ -1,7 +1,18 @@
1
- 安装:在 DSH「插件 → 添加插件」填写 `@ai-workbench/connect@0.7.7`,或运行 `dsh plugin --profile desktop add @ai-workbench/connect@0.7.7`。
1
+ 安装:在 DSH「插件 → 添加插件」填写 `@ai-workbench/connect@0.7.11`,或运行 `dsh plugin --profile desktop add @ai-workbench/connect@0.7.11`。
2
2
 
3
3
  # 远程主机连接插件(L)
4
4
 
5
+ **首次安装前置(管理员)**:先在员工本机运行官方 DSH 的用户 HOME 下,向默认 profile 的 `~/.dsh/profiles/desktop/pnpm-workspace.yaml` 合并以下设置,再让员工安装或更新本项目插件。这里的 `~` 不是管理员自己的 HOME,也不是公司主机上的员工 HOME。若 profile 尚未生成,先正常打开官方 DSH 一次完成默认初始化,再正常退出该实例后修改;不要手建一个缺少官方默认项的配置文件。在已有 `minimumReleaseAgeExclude` 键下合并,不新增重复 YAML 键;把本项目例外放在清单首条,将已有 `@ai-workbench/…@版本` 逐版本条目合并为这一条;保留其他包的例外和其他设置,不覆盖整个文件:
6
+
7
+ ```yaml
8
+ minimumReleaseAgeExclude:
9
+ - '@ai-workbench/*'
10
+ ```
11
+
12
+ 原因:官方 DSH 0.2.0-rc.2 自带 pnpm 11.7.0,其默认新版本等待期为 1440 分钟。同名例外按第一条匹配规则决定;例如先有 `@ai-workbench/connect@0.7.5`,后追加 `@ai-workbench/connect@0.7.6`,后者仍可能被旧规则挡住,锁文件检查甚至会阻塞该 profile 的其他插件安装/更新(D17)。首次安装可能成功,提前配置是为了使后续快速更新也能正常完成。本项仅对本项目 scope 豁免发布时间等待,不关闭其他包的保护,也不把全局等待期改为0。[pnpm 设置说明](https://pnpm.io/settings/dependency-resolution#minimumreleaseageexclude)
13
+
14
+ **能否自动写入**:当前不能由首次安装中的 L 自举完成。官方安装流程先执行 pnpm,成功后才启用插件;L 的运行代码来不及修复阻止自身安装的规则。当前公开安装接口未提供由待安装插件执行的前置配置钩子,本包也无安装脚本。无需改 DSH 核心的可行层级是管理员在安装前预配置 profile;本轮只评估、未实现自动准备器或改员工配置。这是管理员部署前置,员工不需填写主机、密钥或技术参数。核验依据见 [D17 源码证据](docs/evidence/2026-10-22-L-install-D17/RESULT.md)。
15
+
5
16
  员工照常使用官方 DSH 默认界面、数据目录与profile。本插件只提供“设置 → 远程主机连接”一节,管理本项目自己的连接资料、工作区映射与Claude/Codex登录;不限制、拦截或记录其他模型,不改变默认布局或服务。
6
17
 
7
18
  本轮只做中文,已有英文名称保留。A已冻结,Claude/Codex由X的ACP插件承载;L的消费方为X和N。
@@ -12,16 +23,62 @@
12
23
 
13
24
  D13已通过[PR15](https://github.com/yzsnstotz/ai-workbench-connect/pull/15)合推main7915df5,移除前为909eabb;五项被拒产物及清单引用已撤除。不再生成新的聚合整包或验收ZIP,交付物为插件本身。
14
25
 
15
- **插件版本0.7.6,API保持0.7.0/native-local-v1。** N0.4.0已接,配套X0.7.1。L导入动作固定使用官方系统目录窗口:自动能力为native时直接消费其pick;SSH等启动上下文提供browse时,明确调用同一官方原生包的公开pickNativeDirectory,不修改全局选择器。失败原因与临时登录路径只在本节技术详情准确投影。js-yaml/yauzl及其传递运行依赖随tgz打包,官方宿主peer由已安装官方程序提供,员工不另装npm依赖。真实中文GUI/授权及leo E门分别留证。旧140测试/46图只属于前任draft.3;本轮证据独立记录于[第2轮证据](docs/evidence/2026-10-21-L-round2-draft4/)。不得拿旧0.6.3 tgz当native-local-v1的输入。
26
+ **插件版本0.7.11,API保持0.7.0/native-local-v1。** 本轮沿用现有N/X公开API,真实组合由编排验证。L导入动作固定使用官方系统目录窗口:自动能力为native时直接消费其pick;SSH等启动上下文提供browse时,明确调用同一官方原生包的公开pickNativeDirectory,不修改全局选择器。失败原因与临时登录路径只在本节技术详情准确投影。js-yaml/yauzl及其传递运行依赖随tgz打包,官方宿主peer由已安装官方程序提供,员工不另装npm依赖。真实中文GUI/授权及leo E门分别留证。旧140测试/46图只属于前任draft.3;本轮证据独立记录于[第2轮证据](docs/evidence/2026-10-21-L-round2-draft4/)。不得拿旧0.6.3 tgz当native-local-v1的输入。
16
27
 
17
28
  开发使用Node24.13.1,依赖/缓存/构建和临时HOME放自有外接APFS。工程检查为`npm test`、`npm run check`、`npm run build`;它们不替代真实产品路径。安装走官方插件管理,本包自身只声明一个官方bundle条目,不使用wrapper或员工启动器。工人GUI按[SELFTEST](docs/SELFTEST.md)的PID和三项获准偏离执行,不操作owner实例。
18
29
 
19
30
  员工路径见[首次设置](docs/CONNECT-SETUP.md),设置与后果见[SETTINGS-OWNER](docs/SETTINGS-OWNER.md),冻结门见[本轮计划](docs/plans/2026-10-21-L-round2-implementation.md)。
20
31
 
21
- L0.7.4按owner最新纠正分开供给和授权:Electron组件从自身官方地址直接下载,设置显示实际字节/校验/解压;固定版本与SHA不变。供给完成后才启动远端登录与N代理,OAuth页面和回调继续主机出口。系统WebKit探针回环绕过代理,故本轮保留既有Electron内存窗口;不通过重试或多流掩盖隧道慢。
32
+ L0.7.4按owner最新纠正分开供给和授权:Electron组件从自身官方地址直接下载,设置显示实际字节/校验/解压;固定版本与SHA不变。供给完成后才启动远端登录。当前0.7.9中两品牌网页均使用员工本机系统网络;Codex用官方device-auth,网页不经N代理。系统WebKit探针回环绕过代理,故本轮保留既有Electron内存窗口;不通过重试或多流掩盖隧道慢。
22
33
 
23
34
  官方Electron内置Node会把.asar视作虚拟目录;组件缓存/解压专用physical-fs在Electron中使用original-fs核验物理归档,不修改全局process.noAsar。真实官方运行时新建及跨Node复用缓存均有回归。
24
35
 
25
- 0.7.5仅调整公开 npm 发布元数据和包内容:运行依赖继续随包,合同、源码、测试与证据不进包;无安装脚本。源码与合同见[项目仓库](https://github.com/yzsnstotz/ai-workbench-connect)。发布前可用官方插件页或上述命令安装已核验的tgz绝对路径。本轮只做发布准备,尚未发布到npm。
36
+ 0.7.5仅调整公开 npm 发布元数据和包内容:运行依赖继续随包,合同、源码、测试与证据不进包;无安装脚本。源码与合同见[项目仓库](https://github.com/yzsnstotz/ai-workbench-connect)。发布前可用官方插件页或上述命令安装已核验的tgz绝对路径。0.7.11本轮提供固定tgz,由编排决定发布;L不执行publish。
37
+
38
+ 0.7.6:L设置节可显式选择 GitHub 官方源或 npmmirror(Electron 官方文档列出的中国镜像),显示所选来源与理由。未保存选择/未显式配置时默认 npmmirror,依据为编排在leo的可达性实测,不代表自动测速。优先级:本profile设置节已保存选择 → helper.componentDownloadSource初值 → 旧helper.source显式URL → 默认npmmirror;坏设置/保存失败明确报错,不静默切源。预设仍校验原Electron44.0.0固定SHA256。helper.componentObservationIntervalMs默认1000毫秒,只刷新“已N秒无进展”,不触发超时/重试/换源。Claude网页登录的当前流向见下节;Codex本版保留原有N主机代理方式。
39
+
40
+
41
+ ### 0.7.8:Claude 本机网页登录
42
+
43
+ **Claude:网页走本机网络,授权结果经隧道交主机。** 点击本节“登录 Claude”后,主机上本员工自己的`claude auth login --claudeai`生成官方OAuth链接与PKCE上下文;插件的独立临时登录窗口通过员工电脑的系统网络/系统代理打开网页,不为Claude网页申请N代理。员工只在窗口中操作。窗口校验官方回调的state并自动将`code#state`经匿名IPC和现有SSH stdin交给原主机CLI,无需抄码或运行命令;PKCE交换与CLI凭据保存仍只在主机员工HOME。页面出现不等于登录成功,仍以原CLI结果和重新检查为准。窗口会话不持久化,关闭清理本轮临时目录;不改系统代理或其他模型。
44
+
45
+ **0.7.9:Codex 使用官方设备码登录(已知限制:仍需粘贴码)。** 主机员工自己的 `codex login --device-auth` 生成本次设备码;独立登录窗口上方大字显示并提供“复制一次性码”,下方用员工本机系统网络打开 `https://auth.openai.com/codex/device`。员工在窗口登录后,点击复制、粘贴到官方页面,再按官方页面提示确认;不是自动填码,不需要复制命令。官方CLI在主机完成轮询、交换及凭据保存,最终重新检查状态;不监听1455,不自动回退浏览器授权,不排队、不加锁、不限制员工/设备/账号并发数。
46
+
47
+ RFC8628的 `verification_uri_complete` 是可选字段;固定0.163.0 CLI只提供分离的页面地址与码。本轮公开设备码接口HTTP200仅返回device_auth_id/expires_at/interval/user_code,没有完整验证链接或其他URL字段;未找到已公开支持的带码参数,不猜 `?user_code=` 或官方DOM字段。此结论不声称服务器绝不支持未公开行为,依据见[本轮核实](docs/evidence/2026-10-22-L-codex-device-079/RESULT.md)。只采用本单明确授权的复制流程,限制由编排报owner。
48
+
49
+ 一次性码不进argv、环境变量、设置、技术详情或日志;只经匿名SSH/IPC进入本次窗口内存。**只有本人点击复制才写入系统剪贴板**;正常关窗或连接取消时先等本轮复制结束,若剪贴板仍是本次码则清除,已被员工改写则保留;清理失败明确报错。码由官方CLI控制有效期(当前提示15分钟),插件不新增超时、自动重试或新码生成策略。个人账户安全设置/workspace权限可能要求启用device-auth;编排实测的Pro账号可直接完成,不代表所有账户。
50
+
51
+ **登录状态属于主机系统用户,不属于员工设备。** 同一员工多设备映射到该主机用户的不同会话;新设备导入自己的连接资料后,先核实该用户的两品牌状态,已登录直接可用,无登录步骤、不启动主机授权命令(只读status照常);未知也不得启动。仅明确未登录、本人点击后才允许授权,启动前再查。Claude每次授权独享自己的CLI/PKCE/state/stdin,不增加跨用户或跨设备的共享资源、锁或队列。现有设置票据协调只在单个DSH插件实例内存中,无主机/跨设备共享锁;它不是账号并发限制。
52
+
53
+ 此轮无新增设置/默认值/阈值:两品牌遵循系统网络;原N连接与组件配置保持。组件来源/固定Electron版本及SHA256、publishConfig、随包依赖和无安装脚本规则保持。
54
+
55
+
56
+ ### 0.7.9:D19 诊断详情与 D21 可信目录目标
57
+
58
+ 编排已批准优先交付 D19 供 X 消费:完整 profile runtime 接线后,`getContractInfo()` 宣告 `remoteConversationDetails:1`;裸连接服务缺少该 runtime 时不宣告。D21 实现保留,`modelCatalogTarget:1` 仍待确认且不宣告。真实 GUI 与员工产品验收未运行。
59
+
60
+ 诊断仅接纳当前可信连接中、原生会话与工作区仍匹配、且该品牌曾经成功执行真实用户轮核验的事件;诊断追加和历史读取不会触发登录、创建工作区或认领首次说明。多品牌接纳持久保存,旧绑定缺少品牌记录时等待真实用户轮自然核实。事件编号以可信主机系统用户为幂等域;完整 JSON 冲突和序号错误明确拒绝。记录文件600、目录700,完成文件及各层目录同步后才返回记录 ACK;未知保存结果明确失败,同代原事件可核实,跨代使用只读 receipt。断连不读取历史,也不展示旧用户计数。
61
+
62
+ 本节“会话技术详情”按会话与品牌显示总数、未知数、需处理数;逐条展开全字段与完整诊断内容,全部以纯文本展示。“继续加载”保持首页固定快照上界;刷新才重新取首页。详情收起时仍显示需处理提示。SSE 仅发状态/计数,正文由认证后的插件自有 HTTP 端点读取。X 必须先脱敏;L 拒收明确未脱敏认证材料并返回固定原因,不保证从任意文本识别所有秘密。首次说明 delivered 仅显示“已记入可核对回复”,不代表员工已看到。
63
+
64
+ 同一设置节内“管理员设置”实际提供两项数字输入和保存,保存后即时生效并持久到当前 profile;优先级:本节已保存值 → 官方插件 Config 初值 → 默认值。保存失败明确报错,不静默回退。
65
+
66
+ | 设置 | 默认值与范围 | 修改后果 |
67
+ |---|---|---|
68
+ | `conversationDetails.maxEventBytes` | 1048576;1 至 9007199254740991 的整数 | 单个完整 UTF-8 JSON 事件上限。改大增加存储和内存;改小拒绝过大的新事件,不裁剪、不删除旧事件,也不阻止同代相同已存 ID 的幂等核实。 |
69
+ | `conversationDetails.pageSize` | 100;1 至 9007199254740991 的整数 | 每页条数。改大增加一次读取与界面负担;改小增加“继续加载”次数,完整快照计数不变。 |
70
+ | 既有 `enrollmentMetadataMaxBytes` | 1048576;在官方 Config 修改 | 增加诊断设置文件与设置保存 JSON 请求的作用范围,使用本轮实际值;改小可能拒绝设置读取或提交。界面显示当前值。不是诊断总存储上限。 |
71
+
72
+ 诊断记录不自动过期、不按条数淘汰、没有隐藏总条数上限,本版没有清除入口。目录目标查询 `getModelCatalogTarget` 同步返回当前已核验且仍 connected 的内存深副本,不连接、不登录、不查凭据、不绑定项目;projectRoot 元数据不构成模型查询 cwd 授权。
73
+
74
+ 本单工程证据见 [D19/D21](docs/evidence/2026-10-22-L-D19-D21/RESULT.md)。
75
+
76
+
77
+ ### 0.7.10:登录检查失败的技术原因
78
+
79
+ 员工正文和操作入口保持不变。管理员在远程主机连接的技术详情查看失败阶段:进程启动/输入/输出预算、SSH退出或信号、远端结果帧、官方登录状态JSON。保留固定错误类型、系统错误码、退出码、预期键缺失及stderr安全摘要;不显示原始命令输出、异常message或认证材料。消费方需保留已有ConnectNeed.officialReason进入会话技术详情。未知状态仍无法授权,不用等待/重试/放宽校验掩盖。沿用commandOutputMaxBytes,新增设置0;真实根因和产品验收以本轮回执为准。
80
+
81
+
82
+ 0.7.10还修复实际登录检查结果与启动快照不一致:排队品牌queuedProviders与实际检查checkingProviders分开,成功/失败按品牌及当前代次发布;旧结果不能覆盖新结果。认证未知不自动触发反复重连检查。首次绑定前的失败可通过loginDiagnostics:1及五项登录诊断接口持久记录,在同一设置节“登录前技术详情”查看;与工作区绑定和普通会话详情隔离,不启动授权、不创建关联。详见提供方合同0.7.10 P1定稿。
26
83
 
27
- 0.7.6:L设置节可显式选择 GitHub 官方源或 npmmirror(Electron 官方文档列出的中国镜像),显示所选来源与理由。未保存选择/未显式配置时默认 npmmirror,依据为编排在leo的可达性实测,不代表自动测速。优先级:本profile设置节已保存选择 → helper.componentDownloadSource初值 → 旧helper.source显式URL → 默认npmmirror;坏设置/保存失败明确报错,不静默切源。预设仍校验原Electron44.0.0固定SHA256。helper.componentObservationIntervalMs默认1000毫秒,只刷新“已N秒无进展”,不触发超时/重试/换源。OAuth与回调仍走N主机出口。
84
+ 0.7.11:登录前诊断可保留实例不一致时的实际双 ID;只有固定原因与观察字段一致时接纳,当前连接/事件作用域仍严格核验。详见提供方合同的“0.7.10 P1定稿”及“0.7.11:观察到的登录实例ID”两节。D16 旧安装迁移暂停,未包含迁移实现;等待 owner 决定更新前入口。
package/client/index.cjs CHANGED
@@ -40,6 +40,7 @@ window.__ModuleLoader__.load({id:'@ai-workbench/connect',factory:(require)=>{
40
40
  "actionFailed": "The action could not be completed. Contact your administrator."
41
41
  },
42
42
  "zh": {
43
+ "loginNetworkBoundary": "Claude、Codex 登录网页都使用本机网络。Claude 授权结果经隧道交主机;Codex 使用设备码登录,窗口提供大字码和一键复制,仍需粘贴到官方页面。",
43
44
  "title": "远程主机连接",
44
45
  "import": "导入连接资料",
45
46
  "importing": "正在选择并检查连接资料…",
@@ -84,11 +85,11 @@ window.__ModuleLoader__.load({id:'@ai-workbench/connect',factory:(require)=>{
84
85
  const publish=next=>{state=next;for(const fn of listeners)fn();};
85
86
  const accept=(value,source='event')=>{if(disposed)return;const next=value?.startup?.revision,id=value?.startup?.instanceId;if(typeof id!=='string'||!id||!Number.isSafeInteger(next)||next<0){publish({value:undefined,error:'CONNECTION_UNKNOWN'});return;}if(id!==instance){if(instance&&(source==='http'||retired.has(id)))return;if(instance)retired.add(instance);instance=id;revision=-1;}if(next<revision)return;revision=next;const incoming=value.loginComponent?.download;if(incoming){if(typeof incoming.instanceId!=='string'||!Number.isSafeInteger(incoming.revision)||incoming.revision<0||retiredDownloads.has(incoming.instanceId)||(download&&incoming.instanceId===download.instanceId&&incoming.revision<download.revision)||(download&&incoming.instanceId!==download.instanceId&&source==='http')){value={...value,loginComponent:{...value.loginComponent,download}};}else{if(download&&incoming.instanceId!==download.instanceId)retiredDownloads.add(download.instanceId);download=incoming;}}const choice=value.loginComponent?.source;if(choice){if(typeof choice.instanceId!=='string'||!Number.isSafeInteger(choice.revision)||choice.revision<0||retiredSources.has(choice.instanceId)||(componentSource&&choice.instanceId===componentSource.instanceId&&choice.revision<componentSource.revision)||(componentSource&&choice.instanceId!==componentSource.instanceId&&source==='http')){if(componentSource)value={...value,loginComponent:{...value.loginComponent,source:componentSource}};}else{if(componentSource&&choice.instanceId!==componentSource.instanceId)retiredSources.add(componentSource.instanceId);componentSource=choice;}}publish({value,error:value.actionErrorCode||value.enrollment?.need?.code||(value.actionError?'ACTION_FAILED':'')});};
86
87
  const failure=()=>{if(!disposed)publish({value:undefined,error:'CONNECTION_UNKNOWN'});};
87
- return {getSnapshot:()=>state,subscribe:fn=>{listeners.add(fn);return()=>listeners.delete(fn);},async action(path='/check',body){const at=eventVersion;try{const value=await call(path,'POST',body);if(at===eventVersion)accept(value,'http');}catch{if(at===eventVersion)failure();}},start(){if(disposed||started)return;started=true;off=subscribe(value=>{eventVersion++;accept(value);},()=>{eventVersion++;failure();});const at=eventVersion;void call('/status','GET').then(value=>{if(at===eventVersion)accept(value,'http');},()=>{if(at===eventVersion)failure();});},dispose(){disposed=true;off?.();listeners.clear();}};
88
+ return {readLoginDiagnostics:query=>call('/login-diagnostics?'+Object.entries(query).filter(([,v])=>v!==undefined).map(([k,v])=>encodeURIComponent(k)+'='+encodeURIComponent(v)).join('&'),'GET'),readDetails:query=>call('/conversation-details?'+Object.entries(query).filter(([,v])=>v!==undefined).map(([k,v])=>encodeURIComponent(k)+'='+encodeURIComponent(v)).join('&'),'GET'),getSnapshot:()=>state,subscribe:fn=>{listeners.add(fn);return()=>listeners.delete(fn);},async action(path='/check',body){const at=eventVersion;try{const value=await call(path,'POST',body);if(at===eventVersion)accept(value,'http');}catch{if(at===eventVersion)failure();}},start(){if(disposed||started)return;started=true;off=subscribe(value=>{eventVersion++;accept(value);},()=>{eventVersion++;failure();});const at=eventVersion;void call('/status','GET').then(value=>{if(at===eventVersion)accept(value,'http');},()=>{if(at===eventVersion)failure();});},dispose(){disposed=true;off?.();listeners.clear();}};
88
89
  }
89
90
  function useWorkbench(workbench){const snapshot=React.useSyncExternalStore(workbench.subscribe,workbench.getSnapshot);return {state:snapshot.value,error:snapshot.error,action:workbench.action};}
90
91
  function isLoginQuestion(state){const s=state?.startup;return s?.phase==='login-required'&&s.requestId&&s.provider&&s.logins?.[s.provider]===false&&!s.checkingProviders?.includes(s.provider)&&!s.providerNeeds?.[s.provider];}
91
- function Details({state,error,t}){return h('details',null,h('summary',null,t('details')),h('pre',null,JSON.stringify({actionErrorCode:error||state?.actionErrorCode,enrollment:state?.enrollment,actionError:state?.actionError,inventory:state?.inventory,network:state?.network,connect:state?.connect,workspaces:state?.workspaces,checks:state?.startup?.technicalDetails,providerNeeds:state?.startup?.providerNeeds,checkingProviders:state?.startup?.checkingProviders,loginComponent:state?.loginComponent},null,2)));}
92
+ function Details({state,error,t}){return h('details',null,h('summary',null,t('details')),h('pre',null,JSON.stringify({actionErrorCode:error||state?.actionErrorCode,enrollment:state?.enrollment,actionError:state?.actionError,inventory:state?.inventory,network:state?.network,connect:state?.connect,workspaces:state?.workspaces,checks:state?.startup?.technicalDetails,providerNeeds:state?.startup?.providerNeeds,queuedProviders:state?.startup?.queuedProviders,checkingProviders:state?.startup?.checkingProviders,loginComponent:state?.loginComponent},null,2)));}
92
93
  function Choices({state,action,t}){if(!isLoginQuestion(state))return null;const brand=state.startup.provider==='claude'?'Claude':'Codex';return h('div',null,h('button',{type:'button',onClick:()=>void action('/respond/'+state.startup.requestId+'/authorize')},t('signIn',{brand})),h('button',{type:'button',onClick:()=>void action('/respond/'+state.startup.requestId+'/defer')},t('later')));}
93
94
  function Enrollment({state,action,t}){const value=state?.enrollment;if(!value)return null;return h('div',null,h('p',null,t('importHelp')),value.phase==='imported'?h('p',null,t('imported')):h('button',{type:'button',disabled:value.phase==='importing'||['ENROLLMENT_RESULT_UNKNOWN','ENROLLMENT_REJECTED'].includes(value.need?.code),onClick:()=>void action('/enrollment/import')},t(value.phase==='importing'?'importing':'import')));}
94
95
  function observeNotice(node,notify){
@@ -113,11 +114,38 @@ window.__ModuleLoader__.load({id:'@ai-workbench/connect',factory:(require)=>{
113
114
  return h('div',null,...items.map(item=>h(WorkspaceNoticeRow,{key:item.notice.noticeId,item,onVisible:visible})));
114
115
  }
115
116
  function ComponentSource({state,action}){const c=state?.loginComponent?.source;if(!c)return null;const busy=c.changing||c.phase!=='ready'||['downloading','verifying','unpacking'].includes(state?.loginComponent?.download?.phase)||state?.startup?.phase==='authorizing';return h('div',null,h('p',null,c.phase==='ready'?'登录组件下载源:'+c.selectedLabel:c.phase==='loading'?'正在读取登录组件下载源设置…':'登录组件下载源设置不可用;未切换到其他来源。'),h('p',null,c.reason),c.selectionErrorCode?h('p',{role:'alert'},c.selectionErrorCode==='COMPONENT_PIN_MISMATCH'?'下载源未更改:当前组件版本或 SHA256 与预设不符,请查看技术详情。':'下载源未更改,请查看技术详情。'):null,...(c.choices??[]).map(choice=>h('button',{key:choice.id,type:'button',disabled:busy||c.selectedId===choice.id,onClick:()=>void action('/component-source/'+choice.id)},'使用 '+choice.label)),h('p',null,'Electron '+c.version+';两项预设使用同一固定 SHA256,不会因下载失败自动换源。'));}
116
- function DownloadProgress({state,action}){const d=state?.loginComponent?.download;const [,refresh]=React.useState(0),interval=state?.loginComponent?.observationIntervalMs,observing=d?.phase==='downloading'&&Number.isFinite(d.lastProgressAt);React.useEffect(()=>{if(!observing||!Number.isSafeInteger(interval)||interval<1||interval>2147483647)return;const timer=setInterval(()=>refresh(x=>x+1),interval);return()=>clearInterval(timer);},[observing,interval,d?.attemptId]);if(!d)return null;const seconds=Number.isFinite(d.lastProgressAt)?Math.max(0,Math.floor((Date.now()-d.lastProgressAt)/1000)):null;const text=d.phase==='downloading'?'正在从'+(d.sourceLabel||'所选地址')+'下载登录组件:已下载 '+d.receivedBytes+' 字节'+(d.totalBytes===null?'(总大小未知)':',共 '+d.totalBytes+' 字节')+'。'+(seconds===null?'':'已 '+seconds+' 秒无进展;仅观察,不会自动重试或换源。')+'登录页面仍经远程主机连接。':d.phase==='verifying'?'正在校验登录组件…':d.phase==='unpacking'?'正在解压登录组件…':d.phase==='failed'?'登录组件准备失败,请查看技术详情;未重试或换源。':d.phase==='cancelled'?'登录组件下载已取消。':null;return text?h(React.Fragment,null,h('p',{role:'status'},text),['downloading','verifying','unpacking'].includes(d.phase)&&d.attemptId?h('button',{type:'button',onClick:()=>void action('/component-download/cancel/'+d.attemptId)},'取消组件下载'):null):null;}
117
- function Panel({workbench,t}){const {state,error,action}=useWorkbench(workbench),message=messageKey(state?.startup),errorKey=error==='ENROLLMENT_REQUIRED'&&state?.enrollment?.phase==='blocked'?'enrollmentKeyMissing':error?needKey(error):undefined,bodyVisible=!['downloading','verifying','unpacking'].includes(state?.loginComponent?.download?.phase)&&(!error||!!state?.startup)&&!(state?.enrollment?.phase==='blocked'&&state.enrollment.need);return h('section',{'aria-label':t('title')},h('h2',null,t('title')),h(ComponentSource,{state,action}),h(DownloadProgress,{state,action}),h(Enrollment,{state,action,t}),bodyVisible?h('p',{role:errorKey===message?'alert':undefined},t(message)):null,errorKey&&(!bodyVisible||errorKey!==message)?h('p',{role:'alert'},t(errorKey)):null,state?h(React.Fragment,null,...['codex','claude'].map(provider=>{const brand=provider==='claude'?'Claude':'Codex',checking=state.startup?.checkingProviders?.includes(provider),unknown=state.startup?.providerNeeds?.[provider]||state.startup?.logins?.[provider]===undefined;return h('div',{key:provider},h('p',null,t('provider',{brand})+': '+t(checking?'statusChecking':unknown?'statusUnknown':state.startup.logins[provider]?'statusSignedIn':'statusSignIn')),state.startup?.logins?.[provider]===false&&!checking&&!unknown&&(!isLoginQuestion(state)||state.startup.provider!==provider)?h('button',{type:'button',disabled:state.startup.phase==='authorizing',onClick:()=>void action('/start-login/'+provider+'/'+state.startup.instanceId+'/'+state.startup.revision)},t('signIn',{brand})):null);}),h(Choices,{state,action,t}),h(WorkspaceNotices,{state,action}),h('p',null,t('workspaceLimit'))):null,h(Details,{state,error,t}),h('button',{type:'button',onClick:()=>void action()},t('recheck')));}
117
+ function DownloadProgress({state,action}){const d=state?.loginComponent?.download;const [,refresh]=React.useState(0),interval=state?.loginComponent?.observationIntervalMs,observing=d?.phase==='downloading'&&Number.isFinite(d.lastProgressAt);React.useEffect(()=>{if(!observing||!Number.isSafeInteger(interval)||interval<1||interval>2147483647)return;const timer=setInterval(()=>refresh(x=>x+1),interval);return()=>clearInterval(timer);},[observing,interval,d?.attemptId]);if(!d)return null;const seconds=Number.isFinite(d.lastProgressAt)?Math.max(0,Math.floor((Date.now()-d.lastProgressAt)/1000)):null;const text=d.phase==='downloading'?'正在从'+(d.sourceLabel||'所选地址')+'下载登录组件:已下载 '+d.receivedBytes+' 字节'+(d.totalBytes===null?'(总大小未知)':',共 '+d.totalBytes+' 字节')+'。'+(seconds===null?'':'已 '+seconds+' 秒无进展;仅观察,不会自动重试或换源。')+'Claude、Codex 登录网页都使用本机网络;Codex 设备码需复制后粘贴到官方页面。':d.phase==='verifying'?'正在校验登录组件…':d.phase==='unpacking'?'正在解压登录组件…':d.phase==='failed'?'登录组件准备失败,请查看技术详情;未重试或换源。':d.phase==='cancelled'?'登录组件下载已取消。':null;return text?h(React.Fragment,null,h('p',{role:'status'},text),['downloading','verifying','unpacking'].includes(d.phase)&&d.attemptId?h('button',{type:'button',onClick:()=>void action('/component-download/cancel/'+d.attemptId)},'取消组件下载'):null):null;}
118
+ const detailFieldLabels={version:'版本',eventId:'事件编号',instanceId:'连接实例',epoch:'连接代次',sessionId:'会话编号',provider:'品牌',turn:'原生用户轮',step:'原生步骤',sequence:'序号',kind:'类型',severity:'级别',code:'原因码',summary:'摘要',method:'原始方法',payload:'完整诊断内容'};
119
+ const severityLabel=value=>({'info':'信息','warning':'警告','action-required':'需处理'}[value]||value);
120
+ function ConversationDetailRecord({item}){
121
+ return h('details',null,h('summary',null,'序号 '+item.sequence+' · '+severityLabel(item.severity)+' · '+item.summary),h('dl',null,...Object.keys(detailFieldLabels).flatMap(key=>[h('dt',{key:key+'-label'},detailFieldLabels[key]+'('+key+')'),h('dd',{key:key+'-value'},key==='payload'?h('pre',null,JSON.stringify(item.payload,null,2)):item[key]===undefined?'未提供':item[key]===null?'null(官方未提供)':key==='severity'?severityLabel(item[key])+'('+item[key]+')':String(item[key]))])));
122
+ }
123
+ function ConversationDetailSession({session,workbench,loginDiagnostics=false}){
124
+ const [page,setPage]=React.useState(null),[loading,setLoading]=React.useState(false),[error,setError]=React.useState(''),live=React.useRef(true),busy=React.useRef(false);
125
+ React.useEffect(()=>()=>{live.current=false;},[]);
126
+ const load=async more=>{if(busy.current)return;busy.current=true;setLoading(true);setError('');try{
127
+ const result=await (loginDiagnostics?workbench.readLoginDiagnostics:workbench.readDetails)({sessionId:session.sessionId,provider:session.provider,...(more&&page?.nextCursor?{cursor:page.nextCursor}:{})});if(!live.current)return;
128
+ if(!result?.ok){setPage(null);setError(result?.error?.message||'详情读取失败,请确认当前连接;未自动重试。');return;}
129
+ setPage(old=>({...result.value,items:more?[...(old?.items??[]),...result.value.items]:result.value.items}));
130
+ }catch{if(live.current){setPage(null);setError('详情读取失败,请确认当前连接;未自动重试。');}}finally{busy.current=false;if(live.current)setLoading(false);}};
131
+ return h('details',null,h('summary',null,session.sessionId+' · '+(session.provider==='claude'?'Claude':'Codex')+' · 共 '+session.total+' 条,未知 '+session.unknownCount+' 条,'+session.actionRequiredCount+' 项需处理'),h('button',{type:'button',disabled:loading,onClick:()=>load(false)},page?'刷新详情':'读取详情'),loading?h('p',{role:'status'},'正在读取详情…'):null,error?h('p',{role:'alert'},error):null,page?h(React.Fragment,null,h('p',null,'本轮详情快照:共 '+page.total+' 条,未知 '+page.unknownCount+' 条,'+page.actionRequiredCount+' 项需处理。'),...page.items.map(item=>h(ConversationDetailRecord,{key:item.eventId,item})),page.nextCursor?h('button',{type:'button',disabled:loading,onClick:()=>load(true)},'继续加载'):null):null);
132
+ }
133
+ function RemoteConversationDetails({state,workbench,loginDiagnostics=false}){
134
+ const details=loginDiagnostics?state?.loginDiagnostics:state?.conversationDetails;if(!details)return null;
135
+ const count=(details.sessions??[]).reduce((sum,s)=>sum+s.actionRequiredCount,0);
136
+ return h('div',null,count?h('p',{role:'status'},'诊断记录有 '+count+' 项需处理,请展开'+(loginDiagnostics?'登录前技术详情':'会话技术详情')+'查看。'):null,h('details',null,h('summary',null,loginDiagnostics?'登录前技术详情':'会话技术详情'),details.phase==='ready'&&details.need?h('p',{role:'status'},'部分历史记录的原生会话或品牌关联无法核实,当前不显示其计数与正文。'):null,details.phase==='ready'?(details.sessions.length?details.sessions.map(session=>h(ConversationDetailSession,{key:details.instanceId+':'+details.epoch+':'+session.provider+':'+session.sessionId,session,workbench,loginDiagnostics})):h('p',null,'当前可信连接没有可读取的诊断记录。')):h('p',{role:'status'},details.phase==='loading'?'正在核验诊断记录…':details.need?.message||'诊断记录暂时无法读取,请确认当前连接。')));
137
+ }
138
+ function ConversationDetailSettings({state,action}){
139
+ const settings=state?.conversationDetailSettings,[max,setMax]=React.useState(''),[size,setSize]=React.useState(''),[error,setError]=React.useState(''),[saving,setSaving]=React.useState(false);
140
+ React.useEffect(()=>{if(settings?.values){setMax(String(settings.values.maxEventBytes));setSize(String(settings.values.pageSize));}},[settings?.instanceId,settings?.revision]);
141
+ if(!settings)return null;const disabled=saving||settings.phase!=='ready';
142
+ const save=async()=>{const maxEventBytes=Number(max),pageSize=Number(size);if(!/^\d+$/.test(max)||!/^\d+$/.test(size)||![maxEventBytes,pageSize].every(n=>Number.isSafeInteger(n)&&n>0)){setError('设置未保存:两项都需要1至9007199254740991之间的整数。');return;}setError('');setSaving(true);try{await action('/conversation-detail-settings',{maxEventBytes,pageSize});}finally{setSaving(false);}};
143
+ return h('details',null,h('summary',null,'管理员设置'),h('p',null,'仅调整诊断记录。保存后即时生效,重开继续沿用本节保存值;已保存值优先于官方插件 Config 初值。未保存时使用 Config 或以下默认值。'),h('label',null,'conversationDetails.maxEventBytes',h('input',{type:'number',min:1,max:Number.MAX_SAFE_INTEGER,step:1,value:max,disabled,onChange:event=>setMax(event.target.value)})),h('p',null,'完整 UTF-8 JSON 事件字节上限;默认1048576,范围1至9007199254740991整数。改大增加单事件存储与内存;改小拒收超过新值的新事件,不裁剪、不删除已保存记录,也不阻止原事件的幂等核实。'),h('label',null,'conversationDetails.pageSize',h('input',{type:'number',min:1,max:Number.MAX_SAFE_INTEGER,step:1,value:size,disabled,onChange:event=>setSize(event.target.value)})),h('p',null,'一次详情页条数;默认100,范围1至9007199254740991整数。改大增加一次读取和界面负担;改小增加继续加载次数,不减少完整快照总数。'),h('p',null,'enrollmentMetadataMaxBytes 当前为 '+settings.metadataMaxBytes+' 字节(默认1048576),同时限制诊断设置文件与保存提交JSON;调小可能拒绝设置读取或保存,可在官方插件 Config 修改。'),h('p',null,'诊断记录不自动过期、不按条数淘汰、没有隐藏总条数上限;本版没有清除入口。'),h('p',null,settings.phase==='ready'?(settings.source==='saved'?'当前使用本节已保存设置。':'当前使用官方插件 Config 初值或默认值。'):settings.phase==='saving'?'正在保存设置…':'设置无法核实,未自动使用其他值;请联系管理员。'),error?h('p',{role:'alert'},error):null,settings.errorCode?h('p',{role:'alert'},'设置保存或读取失败:'+settings.errorCode):null,h('button',{type:'button',disabled,onClick:save},'保存诊断设置'));
144
+ }
145
+ function Panel({workbench,t}){const {state,error,action}=useWorkbench(workbench),message=messageKey(state?.startup),errorKey=error==='ENROLLMENT_REQUIRED'&&state?.enrollment?.phase==='blocked'?'enrollmentKeyMissing':error?needKey(error):undefined,bodyVisible=!['downloading','verifying','unpacking'].includes(state?.loginComponent?.download?.phase)&&(!error||!!state?.startup)&&!(state?.enrollment?.phase==='blocked'&&state.enrollment.need);return h('section',{'aria-label':t('title')},h('h2',null,t('title')),h('p',null,t('loginNetworkBoundary')),h(ComponentSource,{state,action}),h(DownloadProgress,{state,action}),h(Enrollment,{state,action,t}),bodyVisible?h('p',{role:errorKey===message?'alert':undefined},t(message)):null,errorKey&&(!bodyVisible||errorKey!==message)?h('p',{role:'alert'},t(errorKey)):null,state?h(React.Fragment,null,...['codex','claude'].map(provider=>{const brand=provider==='claude'?'Claude':'Codex',checking=state.startup?.checkingProviders?.includes(provider),unknown=state.startup?.providerNeeds?.[provider]||state.startup?.logins?.[provider]===undefined;return h('div',{key:provider},h('p',null,t('provider',{brand})+': '+t(checking?'statusChecking':unknown?'statusUnknown':state.startup.logins[provider]?'statusSignedIn':'statusSignIn')),state.startup?.logins?.[provider]===false&&!checking&&!unknown&&(!isLoginQuestion(state)||state.startup.provider!==provider)?h('button',{type:'button',disabled:state.startup.phase==='authorizing',onClick:()=>void action('/start-login/'+provider+'/'+state.startup.instanceId+'/'+state.startup.revision)},t('signIn',{brand})):null);}),h(Choices,{state,action,t}),h(WorkspaceNotices,{state,action}),h('p',null,t('workspaceLimit')),...(state.workspaceBindings?.items??[]).filter(item=>item.notice.provider==='delivered').map(item=>h('p',{key:'delivery-'+item.notice.noticeId},item.workspaceName+':首次说明已记入可核对回复。'))):null,h(RemoteConversationDetails,{state,workbench}),h(RemoteConversationDetails,{state,workbench,loginDiagnostics:true}),h(ConversationDetailSettings,{state,action}),h(Details,{state,error,t}),h('button',{type:'button',onClick:()=>void action()},t('recheck')));}
118
146
  return {name:'ai-workbench-connect-client',inject:['slots','locale'],createWorkbenchController,Panel,dictionaries,observeNotice,apply(ctx){
119
147
  ctx.effect(()=>ctx.locale.register(NS,dictionaries));const t=ctx.locale.bind(NS);
120
- const workbench=createWorkbenchController(async(path,method,body)=>{const response=await fetch('/ai-workbench-connect'+path,{method,...body===undefined?{}:{headers:{'Content-Type':'application/json'},body:JSON.stringify(body)}});const value=await response.json();if(!response.ok&&!value?.startup)throw Error('connection-unavailable');return value;},(accept,failure)=>{const source=new EventSource('/ai-workbench-connect/events');source.onmessage=event=>{try{accept(JSON.parse(event.data));}catch{failure();}};source.onerror=failure;return()=>source.close();});
148
+ const workbench=createWorkbenchController(async(path,method,body)=>{const response=await fetch('/ai-workbench-connect'+path,{method,...body===undefined?{}:{headers:{'Content-Type':'application/json'},body:JSON.stringify(body)}});const value=await response.json();if(!response.ok&&!value?.startup&&typeof value?.ok!=='boolean')throw Error('connection-unavailable');return value;},(accept,failure)=>{const source=new EventSource('/ai-workbench-connect/events');source.onmessage=event=>{try{accept(JSON.parse(event.data));}catch{failure();}};source.onerror=failure;return()=>source.close();});
121
149
  ctx.effect(()=>{workbench.start();return()=>workbench.dispose();});
122
150
  ctx.slots.inject('settings.section',()=>ctx.slots.register({name:'settings.section',id:'ai-workbench-connect',label:()=>t('title'),locale:NS,inject:()=>({workbench})},Panel));
123
151
  }};
@@ -1,16 +1,16 @@
1
- // Only official CLI status. No credential files, OAuth, network call or output-text guessing.
2
- const {spawn}=require('node:child_process');const {StringDecoder}=require('node:string_decoder');const decoder=new StringDecoder('utf8');
1
+ // Only official CLI status. Codex exit 1 alone also means a check error; require its fixed official not-signed-in response. No credential files or OAuth.
2
+ const {spawn}=require('node:child_process');const {StringDecoder}=require('node:string_decoder');const decoder=new StringDecoder('utf8'),errorDecoder=new StringDecoder('utf8');
3
3
  const [provider,budgetText]=process.argv.slice(1),budget=Number(budgetText);
4
4
  if(!['codex','claude'].includes(provider)||!Number.isSafeInteger(budget)||budget<1)process.exit(2);
5
- let stdout='',size=0,invalid=false,finished=false;
5
+ let stdout='',stderr='',size=0,invalid=false,finished=false;
6
6
  const child=spawn(provider,provider==='codex'?['login','status']:['auth','status'],{stdio:['ignore','pipe','pipe']});
7
7
  const stop=()=>{invalid=true;child.kill('SIGKILL');};
8
8
  process.on('SIGHUP',stop);process.on('SIGTERM',stop);process.stdout.on('error',stop);child.once('error',()=>{invalid=true;});
9
- for(const [stream,collect] of [[child.stdout,provider==='claude'],[child.stderr,false]])stream.on('data',b=>{size+=b.length;if(size>budget){stop();return;}if(collect)stdout+=decoder.write(b);});
9
+ for(const [stream,channel] of [[child.stdout,'out'],[child.stderr,'err']])stream.on('data',b=>{size+=b.length;if(size>budget){stop();return;}if(channel==='out')stdout+=decoder.write(b);else if(provider==='codex')stderr+=errorDecoder.write(b);});
10
10
  function finish(exitCode){
11
11
  if(finished)return;finished=true;
12
- stdout+=decoder.end();let loggedIn=null;
13
- if(!invalid&&provider==='codex'&&(exitCode===0||exitCode===1))loggedIn=exitCode===0;
12
+ stdout+=decoder.end();stderr+=errorDecoder.end();let loggedIn=null;
13
+ if(!invalid&&provider==='codex'){if(exitCode===0)loggedIn=true;else if(exitCode===1&&stdout.trim()===''&&stderr.trim()==='Not logged in')loggedIn=false;}
14
14
  if(!invalid&&provider==='claude')try{const v=JSON.parse(stdout);if(v&&typeof v==='object'&&!Array.isArray(v)&&typeof v.loggedIn==='boolean')loggedIn=v.loggedIn;}catch(error){process.stderr.write("AUTH_STATUS_JSON_INVALID:"+(error instanceof SyntaxError?"SyntaxError":"ParseError")+"\n");}
15
15
  const code=Number.isInteger(exitCode)&&exitCode>=0&&exitCode<=255?exitCode:1;
16
16
  process.stdout.write(JSON.stringify({protocolVersion:1,provider,exitCode,loggedIn})+'\n',()=>process.exit(code));
@@ -0,0 +1,4 @@
1
+ 'use strict';
2
+ const code=document.getElementById('code'),copy=document.getElementById('copy'),status=document.getElementById('status');
3
+ window.deviceLogin.readCode().then(value=>{code.textContent=value;copy.disabled=false;}).catch(()=>{code.textContent='读取失败';status.textContent='请关闭窗口,回设置查看详情';});
4
+ copy.addEventListener('click',async()=>{try{await window.deviceLogin.copyCode();status.textContent='已复制,请粘贴到下方官方页面';}catch{status.textContent='复制失败,请手动选择上方代码';}});
@@ -0,0 +1 @@
1
+ :root{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;color:#202124;background:#f6f7f9;color-scheme:light}*{box-sizing:border-box}body{margin:0}main{height:160px;padding:16px 24px;border-bottom:1px solid #d7dce2;display:grid;grid-template-columns:1fr auto;gap:8px 24px}.label{font-size:12px;color:#586174;display:block}strong{font:700 32px ui-monospace,SFMono-Regular,Consolas,monospace;letter-spacing:2px;user-select:text}.action{display:flex;flex-direction:column;align-items:center;gap:6px;justify-content:center}button{border:0;border-radius:7px;background:#1759bb;color:#fff;font-size:14px;padding:10px 16px;cursor:pointer}button:disabled{opacity:.5;cursor:default}button:focus-visible{outline:3px solid #9bc0fa;outline-offset:3px}#status{font-size:12px;color:#586174}p{grid-column:1/-1;margin:0;font-size:13px;line-height:1.5;color:#465064}
@@ -0,0 +1 @@
1
+ <!doctype html><html lang="zh-CN"><head><meta charset="UTF-8"><meta http-equiv="Content-Security-Policy" content="default-src 'none'; script-src 'self'; style-src 'self'; base-uri 'none'; form-action 'none'"><title>登录 Codex</title><link rel="stylesheet" href="device-panel.css"></head><body><main><div><span class="label">本次设备码</span><strong id="code" aria-label="本次设备码">正在读取…</strong></div><div class="action"><button id="copy" type="button" disabled>复制一次性码</button><span id="status" role="status">仅复制本次登录的码</span></div><p>下方网页使用本机网络。登录后复制上方代码,粘贴到官方输码框,再按页面提示确认。当前不支持自动填码。</p></main><script src="device-panel.cjs"></script></body></html>
@@ -0,0 +1,5 @@
1
+ const {contextBridge,ipcRenderer}=require('electron');
2
+ const prefix='--aw-device-channel=',arg=process.argv.find(value=>value.startsWith(prefix));
3
+ if(!arg)throw Error('DEVICE_CHANNEL_REQUIRED');
4
+ const channel=arg.slice(prefix.length);
5
+ contextBridge.exposeInMainWorld('deviceLogin',Object.freeze({readCode:()=>ipcRenderer.invoke(channel+'-read'),copyCode:()=>ipcRenderer.invoke(channel+'-copy')}));
@@ -0,0 +1,60 @@
1
+ const {randomUUID}=require('node:crypto');
2
+ const {join}=require('node:path');
3
+ const {pathToFileURL}=require('node:url');
4
+ function bindDeviceExit(app,close,send,signals=process){
5
+ let finishing;
6
+ const unbind=()=>{signals.removeListener('SIGTERM',finish);app.removeListener('before-quit',beforeQuit);};
7
+ const finish=()=>finishing??=(async()=>{
8
+ try{await close();}catch{send({type:'error',code:'CLEANUP_FAILED'});}
9
+ finally{unbind();app.quit();}
10
+ })();
11
+ const beforeQuit=event=>{event.preventDefault();void finish();};
12
+ signals.on('SIGTERM',finish);app.on('before-quit',beforeQuit);return unbind;
13
+ }
14
+ async function openDeviceWindow({app,BrowserWindow,WebContentsView,session,ipcMain,clipboard},boot,send){
15
+ if(boot.provider!=='codex'||boot.url!=='https://auth.openai.com/codex/device'||typeof boot.deviceCode!=='string'||!boot.deviceCode||/[\x00-\x1f\x7f]/.test(boot.deviceCode))throw Error('DEVICE_BOOT_INVALID');
16
+ const ses=session.fromPartition('aw-device-'+randomUUID(),{cache:false});
17
+ await ses.setProxy({mode:'system'});
18
+ ses.setPermissionRequestHandler((_contents,_permission,callback)=>callback(false));
19
+ ses.on('will-download',event=>event.preventDefault());
20
+ const channel='aw-device-'+randomUUID(),panelFile=join(__dirname,'device-panel.html'),panelUrl=pathToFileURL(panelFile).href;
21
+ const win=new BrowserWindow({title:'登录 Codex',show:true,width:900,height:800,webPreferences:{session:ses,preload:join(__dirname,'device-preload.cjs'),additionalArguments:['--aw-device-channel='+channel],nodeIntegration:false,contextIsolation:true,sandbox:true,devTools:false,spellcheck:false}});
22
+ const page=new WebContentsView({webPreferences:{session:ses,nodeIntegration:false,contextIsolation:true,sandbox:true,devTools:false,spellcheck:false}});
23
+ win.contentView.addChildView(page);
24
+ const layout=()=>{const [width,height]=win.getContentSize();page.setBounds({x:0,y:160,width,height:Math.max(0,height-160)});};layout();win.on('resize',layout);
25
+ let closing,copyAttempted=false,unbindExit=()=>{};const pendingCopies=new Set();
26
+ const check=(event,args)=>{
27
+ if(closing||args.length||event.sender!==win.webContents||event.senderFrame!==win.webContents.mainFrame||event.senderFrame.url!==panelUrl)throw Error('DEVICE_IPC_REJECTED');
28
+ };
29
+ ipcMain.handle(channel+'-read',(event,...args)=>{check(event,args);return boot.deviceCode;});
30
+ ipcMain.handle(channel+'-copy',(event,...args)=>{
31
+ check(event,args);copyAttempted=true;const code=boot.deviceCode;
32
+ const operation=Promise.resolve().then(()=>clipboard.writeText(code)).then(()=>true);
33
+ pendingCopies.add(operation);void operation.then(()=>pendingCopies.delete(operation),()=>pendingCopies.delete(operation));return operation;
34
+ });
35
+ const close=()=>closing??=Promise.resolve().then(async()=>{
36
+ try{
37
+ ipcMain.removeHandler(channel+'-read');ipcMain.removeHandler(channel+'-copy');
38
+ // Do not erase a clipboard the employee has since changed. No clipboard history is retained.
39
+ await Promise.allSettled([...pendingCopies]);
40
+ let clipboardFailed=false;
41
+ try{if(copyAttempted&&(await clipboard.readText())===boot.deviceCode)await clipboard.clear();}catch{clipboardFailed=true;}
42
+ boot.deviceCode='';
43
+ if(!page.webContents.isDestroyed())page.webContents.close({waitForBeforeUnload:false});
44
+ if(!win.isDestroyed())win.destroy();
45
+ const cleaned=await Promise.allSettled([ses.closeAllConnections(),ses.clearStorageData(),ses.clearCache()]);
46
+ if(clipboardFailed||cleaned.some(result=>result.status==='rejected'))throw Error('CLEANUP_FAILED');
47
+ }finally{unbindExit();}
48
+ send({type:'closed'});
49
+ });
50
+ if(app)unbindExit=bindDeviceExit(app,close,send);
51
+ win.on('closed',()=>{void close().catch(()=>send({type:'error',code:'CLEANUP_FAILED'}));});
52
+ win.webContents.setWindowOpenHandler(()=>({action:'deny'}));
53
+ win.webContents.on('will-navigate',(event,url)=>{if((event.url??url)!==panelUrl)event.preventDefault();});
54
+ page.webContents.setWindowOpenHandler(({url})=>{if(url.startsWith('https://'))void page.webContents.loadURL(url).catch(()=>send({type:'error',code:'NAVIGATION_FAILED'}));return {action:'deny'};});
55
+ const httpsOnly=(event,url)=>{if(typeof (event.url??url)!=='string'||!(event.url??url).startsWith('https://'))event.preventDefault();};
56
+ page.webContents.on('will-navigate',httpsOnly);page.webContents.on('will-redirect',httpsOnly);
57
+ try{await win.loadFile(panelFile);await page.webContents.loadURL(boot.url);}catch{if(closing){await closing;return {close};}await close();throw Error('NAVIGATION_FAILED');}
58
+ if(!closing)send({type:'ready'});return {close};
59
+ }
60
+ module.exports={openDeviceWindow,bindDeviceExit};
@@ -1,7 +1,8 @@
1
1
  // Public transport adapter. SSH status describes this protocol, not the child command.
2
2
  const {spawn}=require('node:child_process');
3
3
  let request='',child,broken=false;
4
- const stop=()=>{broken=true;if(child?.pid)try{process.kill(-child.pid,'SIGKILL');}catch{}};
4
+ const killChild=()=>{if(child?.pid)try{process.kill(-child.pid,'SIGKILL');}catch(error){if(error?.code!=='ESRCH'){const name=['Error','TypeError','RangeError'].includes(error?.name)?error.name:'UnknownError',code=['EPERM','EACCES','EINVAL'].includes(error?.code)?error.code:'UNKNOWN';process.stderr.write('AW_REMOTE_COMMAND_KILL_FAILED:'+name+':'+code+'\n');process.exitCode=2;}}};
5
+ const stop=()=>{broken=true;killChild();};
5
6
  process.stdout.on('error',stop);process.on('SIGHUP',stop);process.on('SIGTERM',stop);
6
7
  process.stdin.setEncoding('utf8');process.stdin.on('data',b=>request+=b);
7
8
  process.stdin.on('end',()=>{
@@ -10,6 +11,6 @@ process.stdin.on('end',()=>{
10
11
  emit('start');child=spawn('/bin/sh',['-c',r.command],{detached:true,stdio:['pipe','pipe','pipe']});
11
12
  child.once('error',stop);child.stdin.on('error',stop);child.stdin.end(r.input);
12
13
  for(const [stream,type] of [[child.stdout,'stdout'],[child.stderr,'stderr']])stream.on('data',b=>{if(broken)return;if(!emit(type,{data:b.toString('base64')})){stream.pause();process.stdout.once('drain',()=>stream.resume());}});
13
- child.once('exit',()=>{try{process.kill(-child.pid,'SIGKILL');}catch{}});
14
+ child.once('exit',killChild);
14
15
  child.once('close',(exitCode,signal)=>{if(!broken)emit('end',{exitCode,signal});});
15
16
  });
@@ -17,7 +17,7 @@ function signalGroup(signal){if(child?.pid)try{process.kill(-child.pid,signal);}
17
17
  function stop(){if(stopping||finished)return;stopping=true;signalGroup('SIGTERM');if(grace===0)signalGroup('SIGKILL');else killTimer=setTimeout(()=>signalGroup('SIGKILL'),grace);}
18
18
  function finish(code){if(finished)return;finished=true;clearTimeout(killTimer);signalGroup('SIGKILL');input.close();if(temp)rmSync(temp,{recursive:true});process.stdout.write('\nAW_LOGIN_RESULT '+JSON.stringify({protocolVersion:1,provider,exitCode:Number.isInteger(code)&&code>=0&&code<=255?code:null,stopping})+'\n');process.exitCode=code===0&&!stopping?0:1;process.stdin.destroy();}
19
19
  process.on('SIGTERM',stop);process.on('SIGHUP',stop);process.on('SIGINT',stop);process.stdout.on('error',stop);process.stderr.on('error',stop);
20
- child=spawn(provider,provider==='claude'?['auth','login','--claudeai']:['login'],{env,detached:true,stdio:['pipe','pipe','pipe']});
20
+ child=spawn(provider,provider==='claude'?['auth','login','--claudeai']:["login","--device-auth"],{env,detached:true,stdio:['pipe','pipe','pipe']});
21
21
  child.stdin.on('error',stop);child.on('error',()=>finish(1));
22
22
  child.on('spawn',()=>{process.stdout.write('AW_LOGIN_PROCESS '+JSON.stringify({pid:child.pid,group:child.pid,provider})+'\n');});
23
23
  child.stdout.pipe(process.stdout,{end:false});child.stderr.pipe(process.stderr,{end:false});child.on('close',finish);
package/helper/window.cjs CHANGED
@@ -1,26 +1,20 @@
1
1
  const {randomUUID}=require('node:crypto');
2
2
  const {isCallback,createClaudeCapture}=require('./claude-callback.cjs');
3
- async function openWindow({app,BrowserWindow,session},boot,send){
4
- const proxy=new URL(boot.proxy.url);
5
- if(proxy.protocol!=='http:'||proxy.hostname!=='127.0.0.1'||proxy.username||proxy.password)throw new Error('PROXY_INVALID');
3
+ async function openWindow(electron,boot,send){
4
+ const {BrowserWindow,session}=electron;
5
+ if(!['claude','codex'].includes(boot.provider))throw new Error('LOGIN_PROVIDER_INVALID');
6
+ if(boot.provider==='codex')return require('./device-window.cjs').openDeviceWindow(electron,boot,send);
6
7
  const capture=boot.provider==='claude'?createClaudeCapture(boot.url):null;
7
8
  const ses=session.fromPartition('aw-login-'+randomUUID(),{cache:false});
8
- await ses.setProxy({proxyRules:proxy.origin,proxyBypassRules:'<-loopback>'});
9
+ await ses.setProxy({mode:'system'});
9
10
  ses.setPermissionRequestHandler((_contents,_permission,callback)=>callback(false));
10
11
  ses.on('will-download',event=>event.preventDefault());
11
- const login=(event,contents,_details,authInfo,callback)=>{
12
- if(contents?.session!==ses)return;
13
- event.preventDefault();
14
- if(authInfo.isProxy&&authInfo.host===proxy.hostname&&Number(authInfo.port)===Number(proxy.port))callback(boot.proxy.username,boot.proxy.password);
15
- else callback();
16
- };
17
- app.on('login',login);
18
12
  const win=new BrowserWindow({show:true,webPreferences:{session:ses,nodeIntegration:false,contextIsolation:true,sandbox:true,devTools:false,spellcheck:false}});
19
13
  let closing,capturedCode=false;
20
14
  const close=()=>closing??=(async()=>{
21
- app.removeListener('login',login);if(!win.isDestroyed())win.destroy();
15
+ if(!win.isDestroyed())win.destroy();
22
16
  await ses.closeAllConnections();await ses.clearStorageData();await ses.clearCache();
23
- boot.proxy.username='';boot.proxy.password='';send({type:'closed'});
17
+ send({type:'closed'});
24
18
  })();
25
19
  win.on('closed',()=>{void close().catch(()=>send({type:'error',code:'CLEANUP_FAILED'}));});
26
20
  win.webContents.setWindowOpenHandler(({url})=>{
@@ -37,7 +31,7 @@ async function openWindow({app,BrowserWindow,session},boot,send){
37
31
  win.webContents.on('will-navigate',(event,legacyUrl)=>{
38
32
  const url=typeof event.url==='string'?event.url:legacyUrl;
39
33
  captureUrl(url,true,event);
40
- if(typeof url!=='string'||(!url.startsWith('https://')&&!url.startsWith('http://127.0.0.1:1455/')))event.preventDefault();
34
+ if(typeof url!=='string'||!url.startsWith('https://'))event.preventDefault();
41
35
  });
42
36
  win.webContents.on('will-redirect',(event,legacyUrl,_inPlace,legacyMain)=>{
43
37
  captureUrl(typeof event.url==='string'?event.url:legacyUrl,typeof event.isMainFrame==='boolean'?event.isMainFrame:legacyMain,event);
package/lib/api.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  export type Provider = 'claude' | 'codex';
2
- export type NeedCode = 'ACTIVATION_CONFLICT' | 'ACTIVATION_CANCELLED' | 'ENROLLMENT_CLEANUP_UNBOUND' | 'ENROLLMENT_CLEANUP_FAILED' | 'ENROLLMENT_MISMATCH' | 'LOCAL_STATE_INVALID' | 'ALREADY_RUNNING' | 'ENROLLMENT_RECEIPT_REQUIRED' | 'CONTROL_PROTOCOL_INVALID' | 'HELPER_HANDOFF_FAILED' | 'HELPER_SHUTDOWN_FORCED' | 'ENROLLMENT_RESULT_UNKNOWN' | 'ENROLLMENT_REJECTED' | 'ENROLLMENT_REQUIRED' | 'ENROLLMENT_INVALID' | 'NETWORK_KEY_INVALID' | 'HOST_OFFLINE' | 'EMPLOYEE_MISSING' | 'SSH_FAILED' | 'LOGIN_REQUIRED' | 'POLICY_DENIED' | 'CONNECTION_LOST' | 'DEPENDENCY_REQUIRED' | 'SETTINGS_REQUIRED' | 'AUTH_STATUS_INVALID' | 'LOGIN_FAILED' | 'LOGIN_PROMPT_INVALID' | 'DISPOSED' | 'HOST_TRUST_REQUIRED' | 'HOST_KEY_MISMATCH' | 'PROJECT_NAME_INVALID' | 'PROJECT_ROOT_INVALID' | 'PROJECT_EXISTS' | 'PROJECT_INIT_FAILED' | 'PROJECT_CREATE_FAILED' | 'PROJECT_LIST_FAILED' | 'PROJECT_PROTOCOL_INVALID' | 'PROJECT_BINDING_REQUIRED' | 'PROJECT_BINDING_INVALID' | 'PROJECT_BINDING_BUSY' | 'PROJECT_CREATE_UNKNOWN' | 'PROJECT_NOTICE_DELIVERY_UNKNOWN';
2
+ export type NeedCode = 'REMOTE_DETAIL_INVALID' | 'REMOTE_DETAIL_SCOPE_INVALID' | 'REMOTE_DETAIL_STORAGE_FAILED' | 'REMOTE_DETAIL_TOO_LARGE' | 'REMOTE_DETAIL_SEQUENCE_INVALID' | 'REMOTE_DETAIL_CONFLICT' | 'REMOTE_DETAIL_CURSOR_INVALID' | 'MODEL_CATALOG_CONTEXT_INVALID' | 'ACTIVATION_CONFLICT' | 'ACTIVATION_CANCELLED' | 'ENROLLMENT_CLEANUP_UNBOUND' | 'ENROLLMENT_CLEANUP_FAILED' | 'ENROLLMENT_MISMATCH' | 'LOCAL_STATE_INVALID' | 'ALREADY_RUNNING' | 'ENROLLMENT_RECEIPT_REQUIRED' | 'CONTROL_PROTOCOL_INVALID' | 'HELPER_HANDOFF_FAILED' | 'HELPER_SHUTDOWN_FORCED' | 'ENROLLMENT_RESULT_UNKNOWN' | 'ENROLLMENT_REJECTED' | 'ENROLLMENT_REQUIRED' | 'ENROLLMENT_INVALID' | 'NETWORK_KEY_INVALID' | 'HOST_OFFLINE' | 'EMPLOYEE_MISSING' | 'SSH_FAILED' | 'LOGIN_REQUIRED' | 'POLICY_DENIED' | 'CONNECTION_LOST' | 'DEPENDENCY_REQUIRED' | 'SETTINGS_REQUIRED' | 'AUTH_STATUS_INVALID' | 'LOGIN_FAILED' | 'LOGIN_PROMPT_INVALID' | 'DISPOSED' | 'HOST_TRUST_REQUIRED' | 'HOST_KEY_MISMATCH' | 'PROJECT_NAME_INVALID' | 'PROJECT_ROOT_INVALID' | 'PROJECT_EXISTS' | 'PROJECT_INIT_FAILED' | 'PROJECT_CREATE_FAILED' | 'PROJECT_LIST_FAILED' | 'PROJECT_PROTOCOL_INVALID' | 'PROJECT_BINDING_REQUIRED' | 'PROJECT_BINDING_INVALID' | 'PROJECT_BINDING_BUSY' | 'PROJECT_CREATE_UNKNOWN' | 'PROJECT_NOTICE_DELIVERY_UNKNOWN';
3
3
  export interface ConnectNeed {
4
4
  code: NeedCode;
5
5
  layer: 'enrollment' | 'network' | 'ssh' | 'auth' | 'integration' | 'settings' | 'projects';
@@ -85,6 +85,7 @@ export interface LoginPromptState {
85
85
  requestId?: string;
86
86
  provider?: Provider;
87
87
  logins: Partial<Record<Provider, boolean>>;
88
+ queuedProviders?: Provider[];
88
89
  checkingProviders: Provider[];
89
90
  providerNeeds: Partial<Record<Provider, ConnectNeed>>;
90
91
  technicalDetails: {
@@ -102,6 +103,8 @@ export interface ContractInfo {
102
103
  apiVersion: '0.7.0';
103
104
  status: 'implemented';
104
105
  workspaceBinding: 'native-local-v1';
106
+ remoteConversationDetails?: 1;
107
+ loginDiagnostics?: 1;
105
108
  }
106
109
  export interface WorkspaceUse {
107
110
  provider: Provider;
@@ -165,7 +168,101 @@ export interface EnrollmentState {
165
168
  phase: 'empty' | 'importing' | 'imported' | 'blocked';
166
169
  need?: ConnectNeed;
167
170
  }
171
+ export type DetailJson = null | boolean | number | string | DetailJson[] | {
172
+ [key: string]: DetailJson;
173
+ };
174
+ export interface RemoteConversationDetail {
175
+ version: 1;
176
+ eventId: string;
177
+ instanceId: string;
178
+ epoch: number;
179
+ sessionId: string;
180
+ provider: Provider;
181
+ turn: string | number | null;
182
+ step: string | number | null;
183
+ sequence: number;
184
+ kind: string;
185
+ severity: 'info' | 'warning' | 'action-required';
186
+ code: string;
187
+ summary: string;
188
+ method?: string;
189
+ payload: DetailJson;
190
+ }
191
+ export interface RemoteConversationDetailState {
192
+ instanceId: string;
193
+ epoch: number;
194
+ revision: number;
195
+ phase: 'loading' | 'ready' | 'unavailable' | 'failed' | 'disposed';
196
+ sessions: {
197
+ sessionId: string;
198
+ provider: Provider;
199
+ total: number;
200
+ unknownCount: number;
201
+ actionRequiredCount: number;
202
+ }[];
203
+ need?: ConnectNeed;
204
+ }
205
+ export interface RemoteDetailQuery {
206
+ sessionId: string;
207
+ provider: Provider;
208
+ cursor?: string;
209
+ }
210
+ export interface RemoteDetailPage {
211
+ items: RemoteConversationDetail[];
212
+ total: number;
213
+ unknownCount: number;
214
+ actionRequiredCount: number;
215
+ nextCursor?: string;
216
+ }
217
+ export interface ModelCatalogTarget {
218
+ version: 1;
219
+ purpose: 'model-catalog';
220
+ scope: 'host-user';
221
+ instanceId: string;
222
+ epoch: number;
223
+ provider: Provider;
224
+ target: SshTarget;
225
+ }
168
226
  export interface ConnectApi {
227
+ appendLoginDiagnostic(detail: RemoteConversationDetail): Promise<Result<{
228
+ eventId: string;
229
+ recorded: true;
230
+ }>>;
231
+ getLoginDiagnosticReceipt(query: {
232
+ eventId: string;
233
+ sessionId: string;
234
+ provider: Provider;
235
+ }): Promise<Result<{
236
+ eventId: string;
237
+ recorded: false;
238
+ } | {
239
+ eventId: string;
240
+ recorded: true;
241
+ detail: RemoteConversationDetail;
242
+ }>>;
243
+ listLoginDiagnostics(query: RemoteDetailQuery): Promise<Result<RemoteDetailPage>>;
244
+ getLoginDiagnosticState(): RemoteConversationDetailState;
245
+ onLoginDiagnosticState(listener: (state: RemoteConversationDetailState) => void): () => void;
246
+ getModelCatalogTarget(provider: Provider): Result<ModelCatalogTarget>;
247
+ appendRemoteConversationDetail(detail: RemoteConversationDetail): Promise<Result<{
248
+ eventId: string;
249
+ recorded: true;
250
+ }>>;
251
+ getRemoteConversationDetailReceipt(query: {
252
+ eventId: string;
253
+ sessionId: string;
254
+ provider: Provider;
255
+ }): Promise<Result<{
256
+ eventId: string;
257
+ recorded: false;
258
+ } | {
259
+ eventId: string;
260
+ recorded: true;
261
+ detail: RemoteConversationDetail;
262
+ }>>;
263
+ getRemoteConversationDetailState(): RemoteConversationDetailState;
264
+ onRemoteConversationDetailState(listener: (state: RemoteConversationDetailState) => void): () => void;
265
+ listRemoteConversationDetails(query: RemoteDetailQuery): Promise<Result<RemoteDetailPage>>;
169
266
  getContractInfo(): ContractInfo;
170
267
  getEnrollmentState(): EnrollmentState;
171
268
  onEnrollmentState(listener: (state: EnrollmentState) => void): () => void;
@@ -0,0 +1,22 @@
1
+ import type { NeedCode, Result } from './api.ts';
2
+ export declare function validAuthDiagnosticReason(value: unknown): value is string;
3
+ export declare function outputSummary(value: string): {
4
+ cleanupErrors?: {
5
+ errorName: string;
6
+ errorCode: string;
7
+ }[] | undefined;
8
+ bytes: number;
9
+ sha256: string;
10
+ markers: string[];
11
+ };
12
+ export declare function exceptionSummary(error: unknown): {
13
+ errorCode?: string | undefined;
14
+ errorName: string;
15
+ };
16
+ export declare class AuthDiagnosticError extends Error {
17
+ readonly diagnostic: Record<string, unknown>;
18
+ constructor(diagnostic: Record<string, unknown>, message?: string);
19
+ }
20
+ export declare function authFailure(stage: string, reason: string, details?: Record<string, unknown>, error?: unknown, code?: NeedCode): Extract<Result<never>, {
21
+ ok: false;
22
+ }>;
@@ -0,0 +1,45 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { need } from "./needs.js";
3
+ const markers = ['AW_REMOTE_COMMAND_KILL_FAILED', 'AUTH_STATUS_JSON_INVALID:SyntaxError', 'Permission denied', 'Host key verification failed', 'Connection refused', 'Connection closed', 'command not found', 'No such file or directory'];
4
+ const names = ['Error', 'TypeError', 'SyntaxError', 'RangeError', 'AbortError', 'DOMException', 'UnknownError'];
5
+ const codes = ['ENOENT', 'EPERM', 'ESRCH', 'EACCES', 'EINVAL', 'EPIPE', 'ECONNRESET', 'ECONNREFUSED', 'ETIMEDOUT', 'ABORT_ERR', 'UNKNOWN'];
6
+ const stages = ['ssh-status', 'ssh-result', 'remote-frame', 'auth-status', 'connect-check-login', 'integration-pre-login', 'process-spawn', 'process-stdin', 'process-output', 'process-runner'];
7
+ const reasons = ['CANCELLED', 'SSH_EXIT_NONZERO', 'SSH_STDERR_NONEMPTY', 'FRAME_TRUNCATED', 'FRAME_JSON_INVALID', 'FRAME_OBJECT_INVALID', 'FRAME_VERSION_INVALID', 'FRAME_NONCE_INVALID', 'FRAME_AFTER_END', 'FRAME_KEYS_INVALID', 'FRAME_START_MISSING', 'FRAME_BASE64_INVALID', 'REMOTE_COMMAND_SIGNAL', 'FRAME_END_EXIT_INVALID', 'FRAME_SEQUENCE_OR_VALUE_INVALID', 'FRAME_END_MISSING', 'STATUS_ARGUMENT_INVALID', 'STATUS_EXECUTION_FAILED', 'STATUS_JSON_INVALID', 'STATUS_OBJECT_INVALID', 'STATUS_KEYS_INVALID', 'STATUS_VERSION_INVALID', 'STATUS_PROVIDER_INVALID', 'STATUS_EXIT_MISMATCH', 'STATUS_LOGGED_IN_INVALID', 'STATUS_STDERR_NONEMPTY', 'STATUS_EXIT_INVALID', 'STATUS_EXCEPTION', 'STATUS_SUPERSEDED', 'SSH_PORT_MISSING', 'STDIN_FAILED', 'SPAWN_FAILED', 'OUTPUT_LIMIT_EXCEEDED', 'PROCESS_SIGNAL'];
8
+ const object = (v) => !!v && typeof v === 'object' && !Array.isArray(v);
9
+ const integer = (v) => Number.isSafeInteger(v) && Number(v) >= 0;
10
+ function summary(v) { return object(v) && Object.keys(v).every(k => ['bytes', 'sha256', 'markers', 'cleanupErrors'].includes(k)) && integer(v.bytes) && typeof v.sha256 === 'string' && /^[a-f0-9]{64}$/.test(v.sha256) && Array.isArray(v.markers) && v.markers.every((m) => typeof m === 'string' && markers.includes(m)) && (v.cleanupErrors === undefined || Array.isArray(v.cleanupErrors) && v.cleanupErrors.every((e) => object(e) && Object.keys(e).sort().join(',') === 'errorCode,errorName' && ['Error', 'TypeError', 'RangeError', 'UnknownError'].includes(e.errorName) && ['EPERM', 'EACCES', 'EINVAL', 'UNKNOWN'].includes(e.errorCode))); }
11
+ export function validAuthDiagnosticReason(value) {
12
+ if (typeof value !== 'string')
13
+ return false;
14
+ let v;
15
+ try {
16
+ v = JSON.parse(value);
17
+ }
18
+ catch {
19
+ return false;
20
+ }
21
+ if (!object(v) || JSON.stringify(v) !== value || v.version !== 1 || !stages.includes(v.stage) || !reasons.includes(v.reason))
22
+ return false;
23
+ const fields = { version: x => x === 1, stage: x => stages.includes(x), reason: x => reasons.includes(x), errorName: x => names.includes(x), errorCode: x => codes.includes(x), exitCode: x => x === null || typeof x === 'number' && Number.isFinite(x) && Number.isInteger(x), signal: x => ['SIGTERM', 'SIGKILL', 'SIGABRT', 'SIGHUP', 'SIGINT', 'SIGSEGV', 'OTHER'].includes(x), index: integer, unexpectedCount: integer, maxOutputBytes: x => integer(x) && x > 0, observedBytes: integer, stderr: summary, output: summary, missing: x => Array.isArray(x) && x.every(k => ['nonce', 'protocolVersion', 'type', 'data', 'exitCode', 'signal', 'loggedIn', 'provider'].includes(k)), loggedInType: x => ['null', 'undefined', 'object', 'boolean', 'number', 'string', 'function', 'symbol', 'bigint'].includes(x), exitCodeType: x => ['undefined', 'object', 'boolean', 'number', 'string', 'function', 'symbol', 'bigint'].includes(x) };
24
+ return Object.entries(v).every(([key, item]) => Object.hasOwn(fields, key) && fields[key](item));
25
+ }
26
+ // Raw command output and exception messages can contain authentication material.
27
+ // Only fixed markers and content hashes cross the existing technical-detail field.
28
+ export function outputSummary(value) {
29
+ const found = markers.filter(marker => value.includes(marker));
30
+ const cleanupErrors = [...value.matchAll(/AW_REMOTE_COMMAND_KILL_FAILED:(Error|TypeError|RangeError|UnknownError):(EPERM|EACCES|EINVAL|UNKNOWN)(?=\r?\n|$)/g)].map(match => ({ errorName: match[1], errorCode: match[2] }));
31
+ return { bytes: Buffer.byteLength(value), sha256: createHash('sha256').update(value).digest('hex'), markers: found, ...(cleanupErrors.length ? { cleanupErrors } : {}) };
32
+ }
33
+ export function exceptionSummary(error) {
34
+ const value = error;
35
+ return { errorName: typeof value?.name === 'string' && names.includes(value.name) ? value.name : 'UnknownError', ...(typeof value?.code === 'string' && codes.includes(value.code) ? { errorCode: value.code } : {}) };
36
+ }
37
+ export class AuthDiagnosticError extends Error {
38
+ diagnostic;
39
+ constructor(diagnostic, message = 'REMOTE_RESULT_INVALID') { super(message); this.name = 'AuthDiagnosticError'; this.diagnostic = diagnostic; }
40
+ }
41
+ export function authFailure(stage, reason, details = {}, error, code = 'AUTH_STATUS_INVALID') {
42
+ const value = need(code);
43
+ value.officialReason = JSON.stringify({ version: 1, ...(error instanceof AuthDiagnosticError ? error.diagnostic : { stage, reason, ...details, ...(error === undefined ? {} : exceptionSummary(error)) }) });
44
+ return { ok: false, error: value };
45
+ }