@ai-workbench/connect 0.0.0-stage → 0.7.5
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/NOTICE.md +18 -0
- package/README.md +24 -2
- package/client/index.cjs +123 -0
- package/cordis.patch.yml +3 -0
- package/helper/auth-status.cjs +18 -0
- package/helper/claude-callback.cjs +29 -0
- package/helper/main.cjs +19 -0
- package/helper/projects.cjs +57 -0
- package/helper/remote-command.cjs +15 -0
- package/helper/remote-login.cjs +25 -0
- package/helper/window.cjs +52 -0
- package/lib/api.d.ts +196 -0
- package/lib/api.js +1 -0
- package/lib/archive.d.ts +1 -0
- package/lib/archive.js +63 -0
- package/lib/auth-status.d.ts +3 -0
- package/lib/auth-status.js +18 -0
- package/lib/cache.d.ts +21 -0
- package/lib/cache.js +156 -0
- package/lib/component-download.d.ts +25 -0
- package/lib/component-download.js +73 -0
- package/lib/components.d.ts +9 -0
- package/lib/components.js +121 -0
- package/lib/connect.d.ts +10 -0
- package/lib/connect.js +246 -0
- package/lib/cordis.d.ts +15 -0
- package/lib/cordis.js +1 -0
- package/lib/defaults.d.ts +29 -0
- package/lib/defaults.js +4 -0
- package/lib/electron.d.ts +8 -0
- package/lib/electron.js +83 -0
- package/lib/enrollment.d.ts +3 -0
- package/lib/enrollment.js +78 -0
- package/lib/index.d.ts +40 -0
- package/lib/index.js +48 -0
- package/lib/integration.d.ts +5 -0
- package/lib/integration.js +47 -0
- package/lib/login.d.ts +35 -0
- package/lib/login.js +81 -0
- package/lib/native-import-picker.d.ts +11 -0
- package/lib/native-import-picker.js +49 -0
- package/lib/needs.d.ts +9 -0
- package/lib/needs.js +52 -0
- package/lib/network.d.ts +26 -0
- package/lib/network.js +226 -0
- package/lib/physical-fs.d.ts +3 -0
- package/lib/physical-fs.js +7 -0
- package/lib/ports.d.ts +43 -0
- package/lib/ports.js +1 -0
- package/lib/private-files.d.ts +3 -0
- package/lib/private-files.js +41 -0
- package/lib/processes.d.ts +5 -0
- package/lib/processes.js +147 -0
- package/lib/profile-activation.d.ts +32 -0
- package/lib/profile-activation.js +118 -0
- package/lib/profile-enrollment.d.ts +21 -0
- package/lib/profile-enrollment.js +275 -0
- package/lib/profile-runtime.d.ts +6 -0
- package/lib/profile-runtime.js +47 -0
- package/lib/profile-settings.d.ts +2 -0
- package/lib/profile-settings.js +10 -0
- package/lib/projects.d.ts +4 -0
- package/lib/projects.js +41 -0
- package/lib/provider.d.ts +73 -0
- package/lib/provider.js +39 -0
- package/lib/remote-login.d.ts +2 -0
- package/lib/remote-login.js +9 -0
- package/lib/ssh.d.ts +10 -0
- package/lib/ssh.js +78 -0
- package/lib/startup.d.ts +18 -0
- package/lib/startup.js +211 -0
- package/lib/workspace-bindings.d.ts +32 -0
- package/lib/workspace-bindings.js +328 -0
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/node_modules/argparse/LICENSE +254 -0
- package/node_modules/argparse/README.md +84 -0
- package/node_modules/argparse/argparse.js +3707 -0
- package/node_modules/argparse/lib/sub.js +67 -0
- package/node_modules/argparse/lib/textwrap.js +440 -0
- package/node_modules/argparse/package.json +31 -0
- package/node_modules/js-yaml/LICENSE +21 -0
- package/node_modules/js-yaml/README.md +233 -0
- package/node_modules/js-yaml/bin/js-yaml.js +117 -0
- package/node_modules/js-yaml/dist/js-yaml.js +3022 -0
- package/node_modules/js-yaml/dist/js-yaml.js.map +1 -0
- package/node_modules/js-yaml/dist/js-yaml.min.js +2 -0
- package/node_modules/js-yaml/dist/js-yaml.min.js.map +1 -0
- package/node_modules/js-yaml/dist/js-yaml.mjs +3082 -0
- package/node_modules/js-yaml/dist/js-yaml.mjs.map +1 -0
- package/node_modules/js-yaml/index.js +44 -0
- package/node_modules/js-yaml/lib/common.js +50 -0
- package/node_modules/js-yaml/lib/dumper.js +937 -0
- package/node_modules/js-yaml/lib/exception.js +51 -0
- package/node_modules/js-yaml/lib/index_vite_proxy.tmp.mjs +37 -0
- package/node_modules/js-yaml/lib/loader.js +1803 -0
- package/node_modules/js-yaml/lib/schema/core.js +9 -0
- package/node_modules/js-yaml/lib/schema/default.js +20 -0
- package/node_modules/js-yaml/lib/schema/failsafe.js +14 -0
- package/node_modules/js-yaml/lib/schema/json.js +17 -0
- package/node_modules/js-yaml/lib/schema.js +109 -0
- package/node_modules/js-yaml/lib/snippet.js +96 -0
- package/node_modules/js-yaml/lib/type/binary.js +122 -0
- package/node_modules/js-yaml/lib/type/bool.js +35 -0
- package/node_modules/js-yaml/lib/type/float.js +99 -0
- package/node_modules/js-yaml/lib/type/int.js +142 -0
- package/node_modules/js-yaml/lib/type/map.js +8 -0
- package/node_modules/js-yaml/lib/type/merge.js +12 -0
- package/node_modules/js-yaml/lib/type/null.js +35 -0
- package/node_modules/js-yaml/lib/type/omap.js +45 -0
- package/node_modules/js-yaml/lib/type/pairs.js +50 -0
- package/node_modules/js-yaml/lib/type/seq.js +8 -0
- package/node_modules/js-yaml/lib/type/set.js +29 -0
- package/node_modules/js-yaml/lib/type/str.js +8 -0
- package/node_modules/js-yaml/lib/type/timestamp.js +88 -0
- package/node_modules/js-yaml/lib/type.js +66 -0
- package/node_modules/js-yaml/package.json +83 -0
- package/node_modules/pend/LICENSE +23 -0
- package/node_modules/pend/README.md +41 -0
- package/node_modules/pend/index.js +55 -0
- package/node_modules/pend/package.json +18 -0
- package/node_modules/pend/test.js +137 -0
- package/node_modules/yauzl/LICENSE +21 -0
- package/node_modules/yauzl/README.md +1015 -0
- package/node_modules/yauzl/crc32.js +45 -0
- package/node_modules/yauzl/fd-slicer.js +188 -0
- package/node_modules/yauzl/index.js +1096 -0
- package/node_modules/yauzl/package.json +40 -0
- package/package.json +89 -4
package/NOTICE.md
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# 依赖与参考
|
|
2
|
+
|
|
3
|
+
| 名称 | 固定版本 | 协议 | 来源 | 用途 |
|
|
4
|
+
|---|---|---|---|---|
|
|
5
|
+
| DeepSeek Harness | 0.2.0-rc.2 / 639ed015 | MIT | https://github.com/deepseek-ai/deepseek-harness | 只读研究 Cordis 与 bundle;未复制/修改核心 |
|
|
6
|
+
| @deepseek-ai/cordis | 4.0.4 | MIT | 同上 vendor/cordis | DSH 服务/事件,peer;测试用真实 Context |
|
|
7
|
+
| @deepseek-ai/schemastery | 3.18.4 | MIT | 同上 vendor/schemastery | DSH 配置 schema,peer |
|
|
8
|
+
| Electron | 44.0.0 / Chromium152.0.7977.54 | MIT | https://github.com/electron/electron | 独立内存登录窗口;mini真实组件已下载并校验官方ZIP,未捆入L包 |
|
|
9
|
+
| yauzl | 3.4.0 | MIT | https://github.com/thejoshwolfe/yauzl | ZIP 结构与条目读取,安全提取/链接延后创建 |
|
|
10
|
+
| TypeScript | 5.9.3 | Apache-2.0 | https://github.com/microsoft/TypeScript | 类型检查/构建 |
|
|
11
|
+
| @types/node | 24.0.0 | MIT | https://github.com/DefinitelyTyped/DefinitelyTyped | Node 类型 |
|
|
12
|
+
| @types/yauzl | 2.10.3 | MIT | 同上 | ZIP 类型 |
|
|
13
|
+
|
|
14
|
+
Electron API 依据:https://www.electronjs.org/docs/latest/api/session 、https://www.electronjs.org/docs/latest/api/app 。
|
|
15
|
+
extract-zip 2.0.1 因公开 symlink 路径穿越漏洞被移除,未进入最终依赖;yauzl 选用修复 off-by-one 的 3.4.0。最终 npm audit 另存证据。
|
|
16
|
+
未采用 ws:本轮助手用匿名管道,不新增无用途依赖。
|
|
17
|
+
|
|
18
|
+
续轮只读研究官方 @anthropic-ai/claude-code 2.1.20/2.1.304(Anthropic 专有,https://registry.npmjs.org/@anthropic-ai/claude-code )以核实复制码协议;未复制其实现、未作为本包依赖。版本/摘要见续轮 claude-research.json。
|
package/README.md
CHANGED
|
@@ -1,3 +1,25 @@
|
|
|
1
|
-
|
|
1
|
+
安装:在 DSH「插件 → 添加插件」填写 `@ai-workbench/connect@0.7.5`,或运行 `dsh plugin --profile desktop add @ai-workbench/connect@0.7.5`。
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
# 远程主机连接插件(L)
|
|
4
|
+
|
|
5
|
+
员工照常使用官方 DSH 默认界面、数据目录与profile。本插件只提供“设置 → 远程主机连接”一节,管理本项目自己的连接资料、工作区映射与Claude/Codex登录;不限制、拦截或记录其他模型,不改变默认布局或服务。
|
|
6
|
+
|
|
7
|
+
本轮只做中文,已有英文名称保留。A已冻结,Claude/Codex由X的ACP插件承载;L的消费方为X和N。
|
|
8
|
+
|
|
9
|
+
当前合同为[connect-api draft.4](contracts/connect-api.md)。0.7.0已移除旧draft.3的自动sync/registry.create/本机空引用:员工通过DSH原生“添加工作区”和系统目录窗口选择/创建本地文件夹,首次实际远程用户轮才在主机建立或绑定精确同名Git目录。本地文件不会自动上传。持久创建意图防止结果未知时重放;名称冲突、会话换绑和连接代次变化均拒绝。
|
|
10
|
+
|
|
11
|
+
首次说明在L设置节与X普通回复各一次,使用独立持久回执。设置说明按每条实际可视区确认,并在当次打开期间保留;X已确认官方Session落盘核对后以message.id回执,真实组合GUI仍须验证。N消费端已按提供方公开的activate/getActivationState/onActivationState签名实施:L先持久登记及不可变交接,N唯一消费/清理key,无key重开须得到N完整持久身份证明。
|
|
12
|
+
|
|
13
|
+
D13已通过[PR15](https://github.com/yzsnstotz/ai-workbench-connect/pull/15)合推main7915df5,移除前为909eabb;五项被拒产物及清单引用已撤除。不再生成新的聚合整包或验收ZIP,交付物为插件本身。
|
|
14
|
+
|
|
15
|
+
**插件版本0.7.5,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的输入。
|
|
16
|
+
|
|
17
|
+
开发使用Node24.13.1,依赖/缓存/构建和临时HOME放自有外接APFS。工程检查为`npm test`、`npm run check`、`npm run build`;它们不替代真实产品路径。安装走官方插件管理,本包自身只声明一个官方bundle条目,不使用wrapper或员工启动器。工人GUI按[SELFTEST](docs/SELFTEST.md)的PID和三项获准偏离执行,不操作owner实例。
|
|
18
|
+
|
|
19
|
+
员工路径见[首次设置](docs/CONNECT-SETUP.md),设置与后果见[SETTINGS-OWNER](docs/SETTINGS-OWNER.md),冻结门见[本轮计划](docs/plans/2026-10-21-L-round2-implementation.md)。
|
|
20
|
+
|
|
21
|
+
L0.7.4按owner最新纠正分开供给和授权:Electron组件从自身官方地址直接下载,设置显示实际字节/校验/解压;固定版本与SHA不变。供给完成后才启动远端登录与N代理,OAuth页面和回调继续主机出口。系统WebKit探针回环绕过代理,故本轮保留既有Electron内存窗口;不通过重试或多流掩盖隧道慢。
|
|
22
|
+
|
|
23
|
+
官方Electron内置Node会把.asar视作虚拟目录;组件缓存/解压专用physical-fs在Electron中使用original-fs核验物理归档,不修改全局process.noAsar。真实官方运行时新建及跨Node复用缓存均有回归。
|
|
24
|
+
|
|
25
|
+
0.7.5仅调整公开 npm 发布元数据和包内容:运行依赖继续随包,合同、源码、测试与证据不进包;无安装脚本。源码与合同见[项目仓库](https://github.com/yzsnstotz/ai-workbench-connect)。发布前可用官方插件页或上述命令安装已核验的tgz绝对路径。本轮只做发布准备,尚未发布到npm。
|
package/client/index.cjs
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
window.__ModuleLoader__.load({id:'@ai-workbench/connect',factory:(require)=>{
|
|
2
|
+
const React=require('react'),h=React.createElement;
|
|
3
|
+
const NS='ai-workbench-connect';
|
|
4
|
+
const dictionaries={
|
|
5
|
+
"en": {
|
|
6
|
+
"title": "Remote host connection",
|
|
7
|
+
"import": "Import connection files",
|
|
8
|
+
"importing": "Selecting and checking connection files…",
|
|
9
|
+
"enrollmentKeyMissing": "These connection files are missing setup credentials. Ask your administrator to complete them.",
|
|
10
|
+
"imported": "Connection files imported. Network readiness must still be confirmed.",
|
|
11
|
+
"importHelp": "Ask your administrator for your private connection files folder, then choose it here.",
|
|
12
|
+
"enrollmentUnknown": "The previous setup result is unknown. Contact your administrator; connection files will not be submitted again.",
|
|
13
|
+
"enrollmentRejected": "The device setup is no longer valid. Contact your administrator; submission has stopped.",
|
|
14
|
+
"importInvalid": "These connection files could not be verified. Ask your administrator for a valid folder.",
|
|
15
|
+
"workspaceBlocked": "Remote workspaces could not be loaded. Contact your administrator.",
|
|
16
|
+
"workspaceSyncing": "Updating remote workspaces…",
|
|
17
|
+
"workspaceLimit": "Remote workspaces appear in the normal workspace selector after connection. Local files and attachments are not automatically transferred to the remote host.",
|
|
18
|
+
"unknown": "The remote host connection status could not be confirmed. Contact your administrator.",
|
|
19
|
+
"checking": "Checking the remote host connection…",
|
|
20
|
+
"details": "Technical details",
|
|
21
|
+
"signIn": "Sign in to {brand}",
|
|
22
|
+
"later": "Later",
|
|
23
|
+
"recheck": "Check again",
|
|
24
|
+
"provider": "{brand} (remote host)",
|
|
25
|
+
"statusChecking": "Checking",
|
|
26
|
+
"statusUnknown": "Unable to confirm",
|
|
27
|
+
"statusSignedIn": "Signed in",
|
|
28
|
+
"statusSignIn": "Sign in required",
|
|
29
|
+
"loginRequired": "Sign in required. Go to Settings → Remote host connection and choose Sign in.",
|
|
30
|
+
"authorizing": "Complete your sign-in in the authorization window.",
|
|
31
|
+
"complete": "Remote host connection check complete.",
|
|
32
|
+
"loginUnknown": "The sign-in status could not be confirmed. Contact your administrator.",
|
|
33
|
+
"notReady": "The remote host connection is not ready. Contact your administrator.",
|
|
34
|
+
"enrollment": "Connection setup is required. Ask your administrator for the setup files.",
|
|
35
|
+
"settings": "The remote host connection has not been configured. Contact your administrator.",
|
|
36
|
+
"loginFailed": "Sign-in was not completed. You can request sign-in again or try later.",
|
|
37
|
+
"denied": "This account is currently unavailable. Contact your company account administrator.",
|
|
38
|
+
"unavailable": "The remote host is currently unavailable. Contact your administrator.",
|
|
39
|
+
"stale": "This sign-in request has changed. Use the current prompt.",
|
|
40
|
+
"actionFailed": "The action could not be completed. Contact your administrator."
|
|
41
|
+
},
|
|
42
|
+
"zh": {
|
|
43
|
+
"title": "远程主机连接",
|
|
44
|
+
"import": "导入连接资料",
|
|
45
|
+
"importing": "正在选择并检查连接资料…",
|
|
46
|
+
"enrollmentKeyMissing": "这份连接资料缺少开通凭据,请联系管理员补齐。",
|
|
47
|
+
"imported": "连接资料已导入,仍需确认网络是否就绪。",
|
|
48
|
+
"importHelp": "请向管理员领取你的私密连接资料文件夹,然后选择该文件夹。",
|
|
49
|
+
"enrollmentUnknown": "上次开通结果无法确认,请联系管理员核实。已停止再次提交资料。",
|
|
50
|
+
"enrollmentRejected": "开通资料已失效,请联系管理员处理。已停止再次提交。",
|
|
51
|
+
"importInvalid": "这份连接资料无法通过检查,请向管理员领取有效的文件夹。",
|
|
52
|
+
"workspaceBlocked": "远程工作区暂时无法载入,请联系管理员。",
|
|
53
|
+
"workspaceSyncing": "正在更新远程工作区…",
|
|
54
|
+
"workspaceLimit": "通过默认添加工作区选择本地文件夹,首次使用远程模型时才关联同名远程工作区。本地文件不会自动上传。",
|
|
55
|
+
"unknown": "远程主机连接状态无法确认,请联系管理员。",
|
|
56
|
+
"checking": "正在检查远程主机连接…",
|
|
57
|
+
"details": "技术详情",
|
|
58
|
+
"signIn": "登录 {brand}",
|
|
59
|
+
"later": "稍后",
|
|
60
|
+
"recheck": "重新检查",
|
|
61
|
+
"provider": "{brand}(远程主机)",
|
|
62
|
+
"statusChecking": "检查中",
|
|
63
|
+
"statusUnknown": "无法确认",
|
|
64
|
+
"statusSignedIn": "已登录",
|
|
65
|
+
"statusSignIn": "需要登录",
|
|
66
|
+
"loginRequired": "需要登录,请到设置 → 远程主机连接 点登录",
|
|
67
|
+
"authorizing": "请在授权窗口完成本人登录。",
|
|
68
|
+
"complete": "远程主机连接检查完成。",
|
|
69
|
+
"loginUnknown": "登录状态无法确认,请联系管理员。",
|
|
70
|
+
"notReady": "远程主机连接尚未准备好,请联系管理员。",
|
|
71
|
+
"enrollment": "我需要连接资料,请联系管理员发放。",
|
|
72
|
+
"settings": "远程主机连接还没有设置好,请联系管理员。",
|
|
73
|
+
"loginFailed": "登录没有完成。你可以再次请求登录,或稍后再试。",
|
|
74
|
+
"denied": "这个账号暂时不能使用,请联系公司账号管理员。",
|
|
75
|
+
"unavailable": "远程主机暂时连不上,请联系管理员。",
|
|
76
|
+
"stale": "这个登录询问已经更新,请使用当前提示。",
|
|
77
|
+
"actionFailed": "操作未能完成,请联系管理员。"
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
const needKey=code=>code==='ENROLLMENT_RESULT_UNKNOWN'?'enrollmentUnknown':code==='ENROLLMENT_REJECTED'?'enrollmentRejected':code==='CONNECTION_UNKNOWN'?'unknown':code==='ENROLLMENT_INVALID'?'importInvalid':code==='DEPENDENCY_REQUIRED'?'notReady':['ENROLLMENT_REQUIRED','NETWORK_KEY_INVALID'].includes(code)?'enrollment':code==='SETTINGS_REQUIRED'?'settings':code==='LOGIN_FAILED'?'loginFailed':code==='POLICY_DENIED'?'denied':code==='LOGIN_PROMPT_INVALID'?'stale':code==='ACTION_FAILED'?'actionFailed':'unavailable';
|
|
81
|
+
function messageKey(s){if(!s||s.phase==='checking')return 'checking';if(s.phase==='blocked'||s.phase==='disposed')return needKey(s.technicalDetails?.code);if(s.phase==='login-required')return 'loginRequired';if(s.phase==='authorizing')return 'authorizing';return Object.keys(s.providerNeeds??{}).length?'loginUnknown':'complete';}
|
|
82
|
+
function createWorkbenchController(call,subscribe){
|
|
83
|
+
let state={value:undefined,error:''},disposed=false,started=false,off,revision=-1,instance,eventVersion=0;let download;const retiredDownloads=new Set(),retired=new Set(),listeners=new Set();
|
|
84
|
+
const publish=next=>{state=next;for(const fn of listeners)fn();};
|
|
85
|
+
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;}}publish({value,error:value.actionErrorCode||value.enrollment?.need?.code||(value.actionError?'ACTION_FAILED':'')});};
|
|
86
|
+
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
|
+
}
|
|
89
|
+
function useWorkbench(workbench){const snapshot=React.useSyncExternalStore(workbench.subscribe,workbench.getSnapshot);return {state:snapshot.value,error:snapshot.error,action:workbench.action};}
|
|
90
|
+
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 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
|
+
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
|
+
function observeNotice(node,notify){
|
|
95
|
+
if(!node||typeof IntersectionObserver==='undefined'||typeof document==='undefined')return ()=>{};
|
|
96
|
+
let shown=false,intersecting=false;const check=visible=>{if(!shown&&visible&&document.visibilityState==='visible'&&node.isConnected&&node.getClientRects().length){shown=true;notify();}};
|
|
97
|
+
const observer=new IntersectionObserver(rows=>{for(const row of rows)if(row.target===node){intersecting=row.isIntersecting&&row.intersectionRatio>0;check(intersecting);}});observer.observe(node);
|
|
98
|
+
const visible=()=>check(intersecting);document.addEventListener('visibilitychange',visible);
|
|
99
|
+
return()=>{observer.disconnect();document.removeEventListener('visibilitychange',visible);};
|
|
100
|
+
}
|
|
101
|
+
function WorkspaceNoticeRow({item,onVisible}){
|
|
102
|
+
const ref=React.useRef(null);
|
|
103
|
+
React.useEffect(()=>observeNotice(ref.current,()=>onVisible(item)),[item.notice.noticeId]);
|
|
104
|
+
return h('p',{ref},'此工作区对应远程主机上的 '+item.workspaceName+';本地文件不会自动上传。');
|
|
105
|
+
}
|
|
106
|
+
function WorkspaceNotices({state,action}){
|
|
107
|
+
const [retained,setRetained]=React.useState([]),sent=React.useRef(new Set());
|
|
108
|
+
const pending=(state?.workspaceBindings?.items??[]).filter(item=>item.notice.settings==='pending'&&['ready','unavailable'].includes(item.status));
|
|
109
|
+
const items=[...retained,...pending.filter(item=>!retained.some(old=>old.notice.noticeId===item.notice.noticeId))];
|
|
110
|
+
// Retain each sentence during this opening after its independent durable ACK.
|
|
111
|
+
React.useEffect(()=>{if(pending.length)setRetained(old=>[...old,...pending.filter(item=>!old.some(prior=>prior.notice.noticeId===item.notice.noticeId))]);},[pending.map(item=>item.notice.noticeId).join(',')]);
|
|
112
|
+
const visible=item=>{if(!sent.current.has(item.notice.noticeId)){sent.current.add(item.notice.noticeId);void action('/workspace-notice/'+item.notice.noticeId+'/displayed');}};
|
|
113
|
+
return h('div',null,...items.map(item=>h(WorkspaceNoticeRow,{key:item.notice.noticeId,item,onVisible:visible})));
|
|
114
|
+
}
|
|
115
|
+
function DownloadProgress({state}){const d=state?.loginComponent?.download;if(!d)return null;const text=d.phase==='downloading'?'正在从官方地址下载登录组件:已下载 '+d.receivedBytes+' 字节'+(d.totalBytes===null?'(总大小未知)':',共 '+d.totalBytes+' 字节')+'。登录页面仍经远程主机连接。':d.phase==='verifying'?'正在校验登录组件…':d.phase==='unpacking'?'正在解压登录组件…':d.phase==='failed'?'登录组件准备失败,请查看技术详情。':d.phase==='cancelled'?'登录组件下载已取消。':null;return text?h('p',{role:'status'},text):null;}
|
|
116
|
+
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(DownloadProgress,{state}),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
|
+
return {name:'ai-workbench-connect-client',inject:['slots','locale'],createWorkbenchController,Panel,dictionaries,observeNotice,apply(ctx){
|
|
118
|
+
ctx.effect(()=>ctx.locale.register(NS,dictionaries));const t=ctx.locale.bind(NS);
|
|
119
|
+
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();});
|
|
120
|
+
ctx.effect(()=>{workbench.start();return()=>workbench.dispose();});
|
|
121
|
+
ctx.slots.inject('settings.section',()=>ctx.slots.register({name:'settings.section',id:'ai-workbench-connect',label:()=>t('title'),locale:NS,inject:()=>({workbench})},Panel));
|
|
122
|
+
}};
|
|
123
|
+
}});
|
package/cordis.patch.yml
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
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');
|
|
3
|
+
const [provider,budgetText]=process.argv.slice(1),budget=Number(budgetText);
|
|
4
|
+
if(!['codex','claude'].includes(provider)||!Number.isSafeInteger(budget)||budget<1)process.exit(2);
|
|
5
|
+
let stdout='',size=0,invalid=false,finished=false;
|
|
6
|
+
const child=spawn(provider,provider==='codex'?['login','status']:['auth','status'],{stdio:['ignore','pipe','pipe']});
|
|
7
|
+
const stop=()=>{invalid=true;child.kill('SIGKILL');};
|
|
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);});
|
|
10
|
+
function finish(exitCode){
|
|
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;
|
|
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
|
+
const code=Number.isInteger(exitCode)&&exitCode>=0&&exitCode<=255?exitCode:1;
|
|
16
|
+
process.stdout.write(JSON.stringify({protocolVersion:1,provider,exitCode,loggedIn})+'\n',()=>process.exit(code));
|
|
17
|
+
}
|
|
18
|
+
child.once('exit',code=>{if(invalid)finish(code);});child.once('close',finish);
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
const {timingSafeEqual}=require('node:crypto');
|
|
2
|
+
const callbackUri='https://platform.claude.com/oauth/code/callback';
|
|
3
|
+
function parse(value){try{return value instanceof URL?value:new URL(value);}catch{return null;}}
|
|
4
|
+
function isClaudeAuthorizeUrl(value){const u=parse(value);return !!u&&!u.username&&!u.password&&!u.hash&&['https://claude.com/cai/oauth/authorize','https://claude.ai/oauth/authorize','https://platform.claude.com/oauth/authorize'].includes(u.origin+u.pathname);}
|
|
5
|
+
function isCallback(value){const u=parse(value);return !!u&&u.origin+u.pathname===callbackUri;}
|
|
6
|
+
function opaque(value){return typeof value==='string'&&value.length>0&&!/[#\x00-\x20\x7f]/.test(value);}
|
|
7
|
+
function single(url,key){const values=url.searchParams.getAll(key);return values.length===1?values[0]:null;}
|
|
8
|
+
function context(authorize){
|
|
9
|
+
const u=parse(authorize);if(!isClaudeAuthorizeUrl(u))throw new Error('CLAUDE_AUTH_CONTEXT_INVALID');
|
|
10
|
+
const state=single(u,'state');
|
|
11
|
+
if(!opaque(state)||single(u,'response_type')!=='code'||single(u,'redirect_uri')!==callbackUri||single(u,'code_challenge_method')!=='S256'||!opaque(single(u,'code_challenge')))throw new Error('CLAUDE_AUTH_CONTEXT_INVALID');
|
|
12
|
+
return {state};
|
|
13
|
+
}
|
|
14
|
+
function same(a,b){const x=Buffer.from(a),y=Buffer.from(b);return x.length===y.length&&timingSafeEqual(x,y);}
|
|
15
|
+
function validateClaudePaste(authorize,pasted){
|
|
16
|
+
try{const {state}=context(authorize);if(typeof pasted!=='string')return false;const fields=pasted.split('#');return fields.length===2&&opaque(fields[0])&&opaque(fields[1])&&same(fields[1],state);}catch{return false;}
|
|
17
|
+
}
|
|
18
|
+
function createClaudeCapture(authorize){
|
|
19
|
+
const {state}=context(authorize);let done=false;
|
|
20
|
+
return {capture(value){
|
|
21
|
+
const u=parse(value);if(!isCallback(u))return {kind:'unrelated'};
|
|
22
|
+
if(done)return {kind:'duplicate'};done=true;
|
|
23
|
+
const code=single(u,'code'),received=single(u,'state');
|
|
24
|
+
if(u.username||u.password||u.hash||u.searchParams.has('error')||!opaque(code)||!opaque(received))return {kind:'error',error:'CLAUDE_CALLBACK_INVALID'};
|
|
25
|
+
if(!same(received,state))return {kind:'error',error:'CLAUDE_STATE_MISMATCH'};
|
|
26
|
+
return {kind:'code',code:code+'#'+received};
|
|
27
|
+
}};
|
|
28
|
+
}
|
|
29
|
+
module.exports={isClaudeAuthorizeUrl,isCallback,createClaudeCapture,validateClaudePaste};
|
package/helper/main.cjs
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
const {app}=require('electron');
|
|
2
|
+
const {createInterface}=require('node:readline');
|
|
3
|
+
const {openWindow}=require('./window.cjs');
|
|
4
|
+
const input=createInterface({input:process.stdin});let active,started=false;
|
|
5
|
+
const send=event=>{process.stdout.write(JSON.stringify(event)+'\n');if(event.type==='closed')app.quit();};
|
|
6
|
+
input.on('line',async line=>{
|
|
7
|
+
try{
|
|
8
|
+
const message=JSON.parse(line);
|
|
9
|
+
if(started){if(message.type==='close'){await active?.close();app.quit();}return;}
|
|
10
|
+
started=true;
|
|
11
|
+
// Parent creates this private disposable directory; OAuth data remains in a memory session.
|
|
12
|
+
app.setPath('userData',message.userDataDir);app.setPath('sessionData',message.userDataDir);
|
|
13
|
+
app.commandLine.appendSwitch('disable-background-networking');
|
|
14
|
+
app.commandLine.appendSwitch('disable-component-update');
|
|
15
|
+
await app.whenReady();active=await openWindow(require('electron'),message,send);
|
|
16
|
+
}catch{send({type:'error',code:'HELPER_FAILED'});await active?.close();app.quit();}
|
|
17
|
+
});
|
|
18
|
+
input.on('close',async()=>{await active?.close();app.quit();});
|
|
19
|
+
app.on('window-all-closed',()=>{});
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
const fs=require('node:fs/promises');
|
|
2
|
+
const path=require('node:path');
|
|
3
|
+
const {spawnSync}=require('node:child_process');
|
|
4
|
+
function validName(name){return typeof name==='string'&&name!==''&&name!=='.'&&name!=='..'&&!/[\\/\x00-\x1f\x7f]/.test(name);}
|
|
5
|
+
// Repository selection must come solely from trusted root, never the remote shell's Git overrides.
|
|
6
|
+
const gitEnvironment=()=>Object.fromEntries(Object.entries(process.env).filter(([key])=>!key.startsWith('GIT_')));
|
|
7
|
+
const outputPath=text=>text.endsWith('\n')?text.slice(0,-1):text;
|
|
8
|
+
const error=code=>({protocolVersion:1,ok:false,error:{code}});
|
|
9
|
+
async function execute(request){
|
|
10
|
+
if(request?.protocolVersion!==1||!['list','create'].includes(request.operation))return error('PROJECT_PROTOCOL_INVALID');
|
|
11
|
+
if(request.operation==='create'&&!validName(request.name))return error('PROJECT_NAME_INVALID');
|
|
12
|
+
const root=request.root;
|
|
13
|
+
try{
|
|
14
|
+
if(typeof root!=='string'||!path.isAbsolute(root))return error('PROJECT_ROOT_INVALID');
|
|
15
|
+
const st=await fs.lstat(root);
|
|
16
|
+
if(!st.isDirectory()||st.isSymbolicLink()||st.uid!==process.getuid()||(st.mode&0o077)!==0||await fs.realpath(root)!==root)return error('PROJECT_ROOT_INVALID');
|
|
17
|
+
}catch{return error('PROJECT_ROOT_INVALID');}
|
|
18
|
+
if(request.operation==='create'){
|
|
19
|
+
const destination=path.join(root,request.name);let created=false;
|
|
20
|
+
try{
|
|
21
|
+
await fs.mkdir(destination,{mode:0o700});created=true;
|
|
22
|
+
const result=spawnSync('git',['-c','init.templateDir=','init','--quiet','--',destination],{encoding:'utf8',stdio:['ignore','pipe','pipe'],env:gitEnvironment()});
|
|
23
|
+
if(result.status!==0)return error('PROJECT_INIT_FAILED');
|
|
24
|
+
const verified=spawnSync('git',['-C',destination,'rev-parse','--show-toplevel'],{encoding:'utf8',stdio:['ignore','pipe','pipe'],env:gitEnvironment()});
|
|
25
|
+
if(verified.status!==0||outputPath(verified.stdout)!==destination)return error('PROJECT_INIT_FAILED');
|
|
26
|
+
return {protocolVersion:1,ok:true,value:{name:request.name,path:destination}};
|
|
27
|
+
}catch(e){return error(!created&&e.code==='EEXIST'?'PROJECT_EXISTS':'PROJECT_CREATE_FAILED');}
|
|
28
|
+
}
|
|
29
|
+
try{
|
|
30
|
+
const projects=[],unavailable=[];
|
|
31
|
+
for(const name of (await fs.readdir(root)).sort()){
|
|
32
|
+
const directory=path.join(root,name),st=await fs.lstat(directory);let reason;
|
|
33
|
+
if(!validName(name))reason='invalid-name';
|
|
34
|
+
else if(st.isSymbolicLink())reason='symlink';
|
|
35
|
+
else if(!st.isDirectory())reason='not-directory';
|
|
36
|
+
else if(st.uid!==process.getuid()||(st.mode&0o077)!==0)reason='permissions';
|
|
37
|
+
else{
|
|
38
|
+
let metadata=false;try{await fs.lstat(path.join(directory,'.git'));metadata=true;}catch(e){if(e.code!=='ENOENT')throw e;}
|
|
39
|
+
if(!metadata)reason='not-git';
|
|
40
|
+
else{
|
|
41
|
+
const git=spawnSync('git',['-C',directory,'rev-parse','--show-toplevel'],{encoding:'utf8',stdio:['ignore','pipe','pipe'],env:gitEnvironment()});
|
|
42
|
+
if(git.status!==0)reason='invalid-git';
|
|
43
|
+
else if(outputPath(git.stdout)!==directory)reason='outside-root';
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
if(reason)unavailable.push({name,reason});else projects.push({name,path:directory});
|
|
47
|
+
}
|
|
48
|
+
return {protocolVersion:1,ok:true,value:{projects,unavailable}};
|
|
49
|
+
}catch{return error('PROJECT_LIST_FAILED');}
|
|
50
|
+
}
|
|
51
|
+
async function main(){
|
|
52
|
+
let input='';for await(const chunk of process.stdin)input+=chunk;
|
|
53
|
+
let result;try{result=await execute(JSON.parse(input));}catch{result=error('PROJECT_PROTOCOL_INVALID');}
|
|
54
|
+
process.stdout.write(JSON.stringify(result)+'\n');
|
|
55
|
+
}
|
|
56
|
+
module.exports={execute,validName};
|
|
57
|
+
if(require.main===module)void main();
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// Public transport adapter. SSH status describes this protocol, not the child command.
|
|
2
|
+
const {spawn}=require('node:child_process');
|
|
3
|
+
let request='',child,broken=false;
|
|
4
|
+
const stop=()=>{broken=true;if(child?.pid)try{process.kill(-child.pid,'SIGKILL');}catch{}};
|
|
5
|
+
process.stdout.on('error',stop);process.on('SIGHUP',stop);process.on('SIGTERM',stop);
|
|
6
|
+
process.stdin.setEncoding('utf8');process.stdin.on('data',b=>request+=b);
|
|
7
|
+
process.stdin.on('end',()=>{
|
|
8
|
+
if(broken){process.exitCode=2;return;}let r;try{r=JSON.parse(request);if(r.protocolVersion!==1||typeof r.nonce!=='string'||!/^[a-f0-9-]{36}$/.test(r.nonce)||typeof r.command!=='string'||(r.input!==undefined&&typeof r.input!=='string'))throw Error();}catch{process.exitCode=2;return;}request='';
|
|
9
|
+
const emit=(type,fields={})=>process.stdout.write(JSON.stringify({protocolVersion:1,nonce:r.nonce,type,...fields})+'\n');
|
|
10
|
+
emit('start');child=spawn('/bin/sh',['-c',r.command],{detached:true,stdio:['pipe','pipe','pipe']});
|
|
11
|
+
child.once('error',stop);child.stdin.on('error',stop);child.stdin.end(r.input);
|
|
12
|
+
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('close',(exitCode,signal)=>{if(!broken)emit('end',{exitCode,signal});});
|
|
15
|
+
});
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// Public source transported as a fixed node -e program; OAuth values use stdin/stdout only.
|
|
2
|
+
const {spawn}=require('node:child_process');
|
|
3
|
+
const {createInterface}=require('node:readline');
|
|
4
|
+
const {mkdtempSync,mkdirSync,rmSync}=require('node:fs');
|
|
5
|
+
const {homedir}=require('node:os');const {join}=require('node:path');
|
|
6
|
+
const [provider,graceText,mode]=process.argv.slice(1);const grace=Number(graceText);
|
|
7
|
+
if(!['claude','codex'].includes(provider)||!Number.isSafeInteger(grace)||grace<0||grace>2147483647||(mode!==undefined&&mode!=='--probe-isolate'))process.exit(2);
|
|
8
|
+
let temp,child,stopping=false,killTimer,codeSent=false,finished=false;
|
|
9
|
+
const env={...process.env,BROWSER:'true'};
|
|
10
|
+
if(mode==='--probe-isolate'){
|
|
11
|
+
temp=mkdtempSync(join(homedir(),'.aw-l-login-probe-'));
|
|
12
|
+
for(const dir of ['claude','codex','xdg'])mkdirSync(join(temp,dir),{mode:0o700});
|
|
13
|
+
env.CLAUDE_CONFIG_DIR=join(temp,'claude');env.CODEX_HOME=join(temp,'codex');env.XDG_CONFIG_HOME=join(temp,'xdg');
|
|
14
|
+
}
|
|
15
|
+
const input=createInterface({input:process.stdin});
|
|
16
|
+
function signalGroup(signal){if(child?.pid)try{process.kill(-child.pid,signal);}catch(e){if(e.code!=='ESRCH')process.exitCode=1;}}
|
|
17
|
+
function stop(){if(stopping||finished)return;stopping=true;signalGroup('SIGTERM');if(grace===0)signalGroup('SIGKILL');else killTimer=setTimeout(()=>signalGroup('SIGKILL'),grace);}
|
|
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
|
+
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']});
|
|
21
|
+
child.stdin.on('error',stop);child.on('error',()=>finish(1));
|
|
22
|
+
child.on('spawn',()=>{process.stdout.write('AW_LOGIN_PROCESS '+JSON.stringify({pid:child.pid,group:child.pid,provider})+'\n');});
|
|
23
|
+
child.stdout.pipe(process.stdout,{end:false});child.stderr.pipe(process.stderr,{end:false});child.on('close',finish);
|
|
24
|
+
input.on('line',line=>{try{const message=JSON.parse(line);if(message.type==='close'){stop();return;}if(message.type!=='code'||provider!=='claude'||codeSent||typeof message.value!=='string'||!message.value||/[\x00-\x20\x7f]/.test(message.value))throw Error();codeSent=true;child.stdin.end(message.value+'\n');}catch{stop();}});
|
|
25
|
+
input.on('close',stop);
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
const {randomUUID}=require('node:crypto');
|
|
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');
|
|
6
|
+
const capture=boot.provider==='claude'?createClaudeCapture(boot.url):null;
|
|
7
|
+
const ses=session.fromPartition('aw-login-'+randomUUID(),{cache:false});
|
|
8
|
+
await ses.setProxy({proxyRules:proxy.origin,proxyBypassRules:'<-loopback>'});
|
|
9
|
+
ses.setPermissionRequestHandler((_contents,_permission,callback)=>callback(false));
|
|
10
|
+
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
|
+
const win=new BrowserWindow({show:true,webPreferences:{session:ses,nodeIntegration:false,contextIsolation:true,sandbox:true,devTools:false,spellcheck:false}});
|
|
19
|
+
let closing,capturedCode=false;
|
|
20
|
+
const close=()=>closing??=(async()=>{
|
|
21
|
+
app.removeListener('login',login);if(!win.isDestroyed())win.destroy();
|
|
22
|
+
await ses.closeAllConnections();await ses.clearStorageData();await ses.clearCache();
|
|
23
|
+
boot.proxy.username='';boot.proxy.password='';send({type:'closed'});
|
|
24
|
+
})();
|
|
25
|
+
win.on('closed',()=>{void close().catch(()=>send({type:'error',code:'CLEANUP_FAILED'}));});
|
|
26
|
+
win.webContents.setWindowOpenHandler(({url})=>{
|
|
27
|
+
if(url.startsWith('https://'))void win.loadURL(url).catch(()=>send({type:'error',code:'NAVIGATION_FAILED'}));
|
|
28
|
+
return {action:'deny'};
|
|
29
|
+
});
|
|
30
|
+
const captureUrl=(url,main,event)=>{
|
|
31
|
+
if(!capture||main!==true||typeof url!=='string')return;
|
|
32
|
+
const result=capture.capture(url);if(result.kind==='unrelated')return;
|
|
33
|
+
event?.preventDefault?.();
|
|
34
|
+
if(result.kind==='code'){capturedCode=true;send({type:'claude-code',code:result.code});}
|
|
35
|
+
else if(result.kind==='error')send({type:'error',code:result.error});
|
|
36
|
+
};
|
|
37
|
+
win.webContents.on('will-navigate',(event,legacyUrl)=>{
|
|
38
|
+
const url=typeof event.url==='string'?event.url:legacyUrl;
|
|
39
|
+
captureUrl(url,true,event);
|
|
40
|
+
if(typeof url!=='string'||(!url.startsWith('https://')&&!url.startsWith('http://127.0.0.1:1455/')))event.preventDefault();
|
|
41
|
+
});
|
|
42
|
+
win.webContents.on('will-redirect',(event,legacyUrl,_inPlace,legacyMain)=>{
|
|
43
|
+
captureUrl(typeof event.url==='string'?event.url:legacyUrl,typeof event.isMainFrame==='boolean'?event.isMainFrame:legacyMain,event);
|
|
44
|
+
});
|
|
45
|
+
win.webContents.on('did-navigate',(_event,url)=>captureUrl(url,true));
|
|
46
|
+
try{await win.loadURL(boot.url);}catch(error){
|
|
47
|
+
if(!(capturedCode&&(error?.code==='ERR_ABORTED'||error?.errno===-3))){await close();throw new Error('NAVIGATION_FAILED');}
|
|
48
|
+
}
|
|
49
|
+
send({type:'ready'});
|
|
50
|
+
return {close};
|
|
51
|
+
}
|
|
52
|
+
module.exports={openWindow,isCallback};
|
package/lib/api.d.ts
ADDED
|
@@ -0,0 +1,196 @@
|
|
|
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';
|
|
3
|
+
export interface ConnectNeed {
|
|
4
|
+
code: NeedCode;
|
|
5
|
+
layer: 'enrollment' | 'network' | 'ssh' | 'auth' | 'integration' | 'settings' | 'projects';
|
|
6
|
+
message: string;
|
|
7
|
+
requires: string;
|
|
8
|
+
actor: 'employee' | 'administrator' | 'ai-administrator' | 'plugin';
|
|
9
|
+
action: {
|
|
10
|
+
id: string;
|
|
11
|
+
label: string;
|
|
12
|
+
available: boolean;
|
|
13
|
+
};
|
|
14
|
+
officialReason?: string;
|
|
15
|
+
lastSeen?: string;
|
|
16
|
+
}
|
|
17
|
+
export type Result<T> = {
|
|
18
|
+
ok: true;
|
|
19
|
+
value: T;
|
|
20
|
+
} | {
|
|
21
|
+
ok: false;
|
|
22
|
+
error: ConnectNeed;
|
|
23
|
+
};
|
|
24
|
+
export interface SessionTarget {
|
|
25
|
+
command: string;
|
|
26
|
+
root: string;
|
|
27
|
+
settingsPath: string;
|
|
28
|
+
protocolVersion: 1;
|
|
29
|
+
}
|
|
30
|
+
export interface HostContext {
|
|
31
|
+
enforcement: 'production' | 'test';
|
|
32
|
+
checkExitCode: 0 | 1;
|
|
33
|
+
failedChecks: {
|
|
34
|
+
id: string;
|
|
35
|
+
status: string;
|
|
36
|
+
}[];
|
|
37
|
+
unverifiedChecks: {
|
|
38
|
+
id: string;
|
|
39
|
+
status: string;
|
|
40
|
+
}[];
|
|
41
|
+
}
|
|
42
|
+
export interface SshTarget {
|
|
43
|
+
hostContext?: HostContext;
|
|
44
|
+
host: string;
|
|
45
|
+
username: string;
|
|
46
|
+
port: number;
|
|
47
|
+
proxyCommand: string;
|
|
48
|
+
projectRoot: string;
|
|
49
|
+
knownHostsFile: string;
|
|
50
|
+
session: SessionTarget;
|
|
51
|
+
}
|
|
52
|
+
export interface LoginState {
|
|
53
|
+
provider: Provider;
|
|
54
|
+
loggedIn: boolean;
|
|
55
|
+
}
|
|
56
|
+
export type Phase = 'idle' | 'enrolling' | 'network' | 'ssh' | 'connected' | 'checking-login' | 'login' | 'ready' | 'blocked' | 'disposed';
|
|
57
|
+
export interface ConnectState {
|
|
58
|
+
phase: Phase;
|
|
59
|
+
target?: SshTarget;
|
|
60
|
+
provider?: Provider;
|
|
61
|
+
need?: ConnectNeed;
|
|
62
|
+
}
|
|
63
|
+
export interface Project {
|
|
64
|
+
name: string;
|
|
65
|
+
path: string;
|
|
66
|
+
}
|
|
67
|
+
export interface ProjectList {
|
|
68
|
+
projects: Project[];
|
|
69
|
+
unavailable: {
|
|
70
|
+
name: string;
|
|
71
|
+
reason: 'invalid-name' | 'symlink' | 'not-directory' | 'permissions' | 'not-git' | 'invalid-git' | 'outside-root';
|
|
72
|
+
}[];
|
|
73
|
+
}
|
|
74
|
+
export interface ConnectionState {
|
|
75
|
+
instanceId: string;
|
|
76
|
+
phase: 'connecting' | 'connected' | 'unavailable' | 'disposed';
|
|
77
|
+
epoch: number;
|
|
78
|
+
need?: ConnectNeed;
|
|
79
|
+
}
|
|
80
|
+
export interface LoginPromptState {
|
|
81
|
+
instanceId: string;
|
|
82
|
+
revision: number;
|
|
83
|
+
phase: 'checking' | 'blocked' | 'login-required' | 'authorizing' | 'idle' | 'disposed';
|
|
84
|
+
message: string;
|
|
85
|
+
requestId?: string;
|
|
86
|
+
provider?: Provider;
|
|
87
|
+
logins: Partial<Record<Provider, boolean>>;
|
|
88
|
+
checkingProviders: Provider[];
|
|
89
|
+
providerNeeds: Partial<Record<Provider, ConnectNeed>>;
|
|
90
|
+
technicalDetails: {
|
|
91
|
+
code?: NeedCode;
|
|
92
|
+
hostContext?: HostContext;
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
export interface LoginPromptResult {
|
|
96
|
+
requestId: string;
|
|
97
|
+
provider: Provider;
|
|
98
|
+
outcome: 'deferred' | 'logged-in' | 'failed' | 'invalidated';
|
|
99
|
+
need?: ConnectNeed;
|
|
100
|
+
}
|
|
101
|
+
export interface ContractInfo {
|
|
102
|
+
apiVersion: '0.7.0';
|
|
103
|
+
status: 'implemented';
|
|
104
|
+
workspaceBinding: 'native-local-v1';
|
|
105
|
+
}
|
|
106
|
+
export interface WorkspaceUse {
|
|
107
|
+
provider: Provider;
|
|
108
|
+
purpose: 'user-turn';
|
|
109
|
+
}
|
|
110
|
+
export interface WorkspaceNotice {
|
|
111
|
+
noticeId: string;
|
|
112
|
+
claimId: string;
|
|
113
|
+
workspaceName: string;
|
|
114
|
+
messageKey: 'remote-workspace-binding';
|
|
115
|
+
}
|
|
116
|
+
export interface SessionProjectTarget {
|
|
117
|
+
instanceId: string;
|
|
118
|
+
epoch: number;
|
|
119
|
+
bindingRevision: number;
|
|
120
|
+
bindingId: string;
|
|
121
|
+
sessionId: string;
|
|
122
|
+
workspaceId: string;
|
|
123
|
+
project: Project;
|
|
124
|
+
target: SshTarget;
|
|
125
|
+
notice?: WorkspaceNotice;
|
|
126
|
+
}
|
|
127
|
+
export interface WorkspaceBindingState {
|
|
128
|
+
instanceId: string;
|
|
129
|
+
revision: number;
|
|
130
|
+
items: {
|
|
131
|
+
bindingId: string;
|
|
132
|
+
workspaceId: string;
|
|
133
|
+
workspaceName: string;
|
|
134
|
+
status: 'ready' | 'unavailable' | 'invalid' | 'pending';
|
|
135
|
+
notice: {
|
|
136
|
+
noticeId: string;
|
|
137
|
+
settings: 'pending' | 'displayed';
|
|
138
|
+
provider: 'pending' | 'claimed' | 'delivered' | 'unknown';
|
|
139
|
+
};
|
|
140
|
+
}[];
|
|
141
|
+
}
|
|
142
|
+
export type WorkspaceNoticeReceipt = {
|
|
143
|
+
noticeId: string;
|
|
144
|
+
surface: 'settings';
|
|
145
|
+
outcome: 'displayed';
|
|
146
|
+
} | {
|
|
147
|
+
noticeId: string;
|
|
148
|
+
surface: 'provider';
|
|
149
|
+
claimId: string;
|
|
150
|
+
sessionId: string;
|
|
151
|
+
provider: Provider;
|
|
152
|
+
outcome: 'delivered';
|
|
153
|
+
deliveryId: string;
|
|
154
|
+
} | {
|
|
155
|
+
noticeId: string;
|
|
156
|
+
surface: 'provider';
|
|
157
|
+
claimId: string;
|
|
158
|
+
sessionId: string;
|
|
159
|
+
provider: Provider;
|
|
160
|
+
outcome: 'not-delivered' | 'unknown';
|
|
161
|
+
};
|
|
162
|
+
export interface EnrollmentState {
|
|
163
|
+
instanceId: string;
|
|
164
|
+
revision: number;
|
|
165
|
+
phase: 'empty' | 'importing' | 'imported' | 'blocked';
|
|
166
|
+
need?: ConnectNeed;
|
|
167
|
+
}
|
|
168
|
+
export interface ConnectApi {
|
|
169
|
+
getContractInfo(): ContractInfo;
|
|
170
|
+
getEnrollmentState(): EnrollmentState;
|
|
171
|
+
onEnrollmentState(listener: (state: EnrollmentState) => void): () => void;
|
|
172
|
+
importEnrollment(): Promise<Result<{
|
|
173
|
+
outcome: 'imported' | 'cancelled';
|
|
174
|
+
state: EnrollmentState;
|
|
175
|
+
}>>;
|
|
176
|
+
getWorkspaceBindingState(): WorkspaceBindingState;
|
|
177
|
+
onWorkspaceBindingState(listener: (state: WorkspaceBindingState) => void): () => void;
|
|
178
|
+
acknowledgeWorkspaceNotice(receipt: WorkspaceNoticeReceipt): Promise<Result<void>>;
|
|
179
|
+
resolveSessionProject(sessionId: string, use: WorkspaceUse): Promise<Result<SessionProjectTarget>>;
|
|
180
|
+
getConnectionState(): ConnectionState;
|
|
181
|
+
onConnectionState(listener: (state: ConnectionState) => void): () => void;
|
|
182
|
+
getLoginPromptState(): LoginPromptState;
|
|
183
|
+
onLoginPromptState(listener: (state: LoginPromptState) => void): () => void;
|
|
184
|
+
requestLoginPrompt(provider: Provider, intent: 'automatic' | 'user'): Promise<Result<LoginPromptState>>;
|
|
185
|
+
respondLoginPrompt(requestId: string, action: 'authorize' | 'defer'): Promise<Result<LoginPromptState>>;
|
|
186
|
+
refreshLoginPrompts(): Promise<Result<LoginPromptState>>;
|
|
187
|
+
listProjects(): Promise<Result<ProjectList>>;
|
|
188
|
+
createProject(name: string): Promise<Result<Project>>;
|
|
189
|
+
getState(): ConnectState;
|
|
190
|
+
connect(): Promise<Result<SshTarget>>;
|
|
191
|
+
getSshTarget(): Promise<Result<SshTarget>>;
|
|
192
|
+
checkLogin(provider: Provider): Promise<Result<LoginState>>;
|
|
193
|
+
login(provider: Provider): Promise<Result<LoginState>>;
|
|
194
|
+
ensureReady(provider: Provider): Promise<Result<SshTarget>>;
|
|
195
|
+
dispose(): Promise<void>;
|
|
196
|
+
}
|
package/lib/api.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/lib/archive.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function unpackZip(zipPath: string, root: string, maxBytes: number, signal?: AbortSignal): Promise<void>;
|