@hellolynk-ai/codex-plugin 0.2.104 → 0.2.105-macos.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +17 -17
- package/bin/hellolynk-client-setup.mjs +0 -0
- package/hellolynk-client-pack-manifest.json +44 -24
- package/hellolynk-client-pack-pin.json +1 -1
- package/package.json +8 -4
- package/plugins/hellolynk-codex/.codex-plugin/plugin.json +1 -1
- package/plugins/hellolynk-codex/.mcp.json +20 -19
- package/plugins/hellolynk-codex/package.json +1 -1
- package/plugins/hellolynk-codex/runtime/bootstrap/core/hellolynk-client-core.mjs +64 -14
- package/plugins/hellolynk-codex/runtime/bootstrap/core/hellolynk-platform-credential-store.mjs +32 -0
- package/plugins/hellolynk-codex/runtime/bootstrap/platform/darwin/hellolynk-keychain-helper.bin +0 -0
- package/plugins/hellolynk-codex/runtime/bootstrap/platform/darwin/hellolynk-keychain-helper.swift +149 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-client-core.mjs +64 -14
- package/plugins/hellolynk-codex/runtime/core/hellolynk-neutral-host-assembly.mjs +5 -18
- package/plugins/hellolynk-codex/runtime/core/hellolynk-neutral-host-process.mjs +1 -1
- package/plugins/hellolynk-codex/runtime/core/hellolynk-platform-credential-store.mjs +32 -0
- package/plugins/hellolynk-codex/runtime/neutral-host-release-manifest.json +24 -8
- package/plugins/hellolynk-codex/runtime/neutral-host-release-pin.json +1 -1
- package/plugins/hellolynk-codex/runtime/platform/darwin/hellolynk-keychain-helper.bin +0 -0
- package/plugins/hellolynk-codex/runtime/platform/darwin/hellolynk-keychain-helper.swift +149 -0
- package/plugins/hellolynk-codex/server/codex-neutral-production.mjs +3 -3
package/README.md
CHANGED
|
@@ -35,35 +35,35 @@
|
|
|
35
35
|
|
|
36
36
|
Codex、OpenClaw、TRAE Work、WorkBuddy。装哪个客户端,就选哪个包。同一个账号下,它们共用你的灵信号。
|
|
37
37
|
|
|
38
|
-
源码见 [Codex 客户端源码](https://github.com/haoyun18881-beep/HelloLynk/tree/main/clients/codex),完整介绍见 [HelloLynk on GitHub](https://github.com/haoyun18881-beep/HelloLynk)。
|
|
38
|
+
源码见 [Codex 客户端源码](https://github.com/haoyun18881-beep/HelloLynk/tree/main/clients/codex-macos),完整介绍见 [HelloLynk on GitHub](https://github.com/haoyun18881-beep/HelloLynk)。
|
|
39
39
|
|
|
40
40
|
## 前提
|
|
41
41
|
|
|
42
|
-
|
|
42
|
+
Apple 芯片、macOS 13 或更高版本、Node.js 22.16 或更高版本,以及安装在「应用程序」中的 ChatGPT(已打开过 Codex)。
|
|
43
43
|
|
|
44
44
|
## 安装
|
|
45
45
|
|
|
46
46
|
1. 打开 [HelloLynk 官网(Official site)](https://hellolynk.com)。新用户注册账号;已有账号登录后,查看自己的公开灵信号。
|
|
47
47
|
2. 两种装法任选一种。
|
|
48
48
|
|
|
49
|
-
|
|
49
|
+
推荐用 npm,在终端运行:
|
|
50
50
|
|
|
51
|
-
```
|
|
52
|
-
|
|
51
|
+
```sh
|
|
52
|
+
npm install -g @hellolynk-ai/codex-plugin@macos
|
|
53
|
+
hellolynk-codex init
|
|
53
54
|
```
|
|
54
55
|
|
|
55
|
-
|
|
56
|
+
用安装包,在官网或 [GitHub Release](https://github.com/haoyun18881-beep/HelloLynk/releases) 下载 Codex for Mac 安装包,完整解压,在包含 `package` 文件夹的目录打开 终端,运行:
|
|
56
57
|
|
|
57
|
-
```
|
|
58
|
-
|
|
59
|
-
hellolynk-codex init
|
|
58
|
+
```sh
|
|
59
|
+
node ./package/bin/hellolynk-client-setup.mjs init
|
|
60
60
|
```
|
|
61
61
|
|
|
62
62
|
3. 完全退出并重新打开 Codex。
|
|
63
63
|
|
|
64
64
|
可以把本说明交给你的本地 AI,请它帮你安装;用安装包时,把解压目录也告诉它。
|
|
65
65
|
|
|
66
|
-
下文命令按安装包写法给出。用 npm 装的,把 `node
|
|
66
|
+
下文命令按安装包写法给出。用 npm 装的,把 `node ./package/bin/hellolynk-client-setup.mjs` 换成 `hellolynk-codex`,在任意目录运行。
|
|
67
67
|
|
|
68
68
|
## 首次连接
|
|
69
69
|
|
|
@@ -73,8 +73,8 @@ Windows 10/11、Node.js 22.16 或更高版本、PowerShell 7,以及打开过
|
|
|
73
73
|
|
|
74
74
|
也可以让 AI 用命令连接,把示例换成自己的公开灵信号:
|
|
75
75
|
|
|
76
|
-
```
|
|
77
|
-
node
|
|
76
|
+
```sh
|
|
77
|
+
node ./package/bin/hellolynk-client-setup.mjs connect --hellolynk-id hellolynk:intl:your-name
|
|
78
78
|
```
|
|
79
79
|
|
|
80
80
|
首次连接这台电脑时,命令会给出官网确认链接。AI 把链接给用户;用户确认后,AI 继续执行同一条连接命令。
|
|
@@ -83,20 +83,20 @@ node .\package\bin\hellolynk-client-setup.mjs connect --hellolynk-id hellolynk:i
|
|
|
83
83
|
|
|
84
84
|
插件会自动更新,不用操作。要手动更新,下载新版安装包,在包含 `package` 文件夹的目录运行:
|
|
85
85
|
|
|
86
|
-
```
|
|
87
|
-
node
|
|
86
|
+
```sh
|
|
87
|
+
node ./package/bin/hellolynk-client-setup.mjs setup
|
|
88
88
|
```
|
|
89
89
|
|
|
90
|
-
用 npm 装的,先运行 `npm install -g @hellolynk-ai/codex-plugin` 装上新版,再运行 `hellolynk-codex setup`。
|
|
90
|
+
用 npm 装的,先运行 `npm install -g @hellolynk-ai/codex-plugin@macos` 装上新版,再运行 `hellolynk-codex setup`。
|
|
91
91
|
|
|
92
92
|
## 卸载
|
|
93
93
|
|
|
94
|
-
在包含 `package` 文件夹的目录运行 `node
|
|
94
|
+
在包含 `package` 文件夹的目录运行 `node ./package/bin/hellolynk-client-setup.mjs uninstall`。账号、联系人和聊天记录留在这台电脑上,重新安装后不用重新连接。
|
|
95
95
|
|
|
96
96
|
用 npm 装的,运行 `hellolynk-codex uninstall`,再运行 `npm uninstall -g @hellolynk-ai/codex-plugin`。
|
|
97
97
|
|
|
98
98
|
## 求助
|
|
99
99
|
|
|
100
|
-
运行 `node
|
|
100
|
+
运行 `node ./package/bin/hellolynk-client-setup.mjs doctor` 检查安装,把结果和错误码发到 [haoyun18881@gmail.com](mailto:haoyun18881@gmail.com)。
|
|
101
101
|
|
|
102
102
|
许可:PolyForm Shield License 1.0.0,只覆盖本包内的客户端源码,完整条款见 LICENSE。
|
|
File without changes
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"contract": "hellolynk-client-npm-pack-manifest-v1",
|
|
3
3
|
"client": "codex",
|
|
4
4
|
"packageName": "@hellolynk-ai/codex-plugin",
|
|
5
|
-
"version": "0.2.
|
|
5
|
+
"version": "0.2.105-macos.1",
|
|
6
6
|
"workbench": {
|
|
7
7
|
"path": "plugins/hellolynk-codex/runtime/workbench/hellolynk-workbench-build.json",
|
|
8
8
|
"contract": "hellolynk-shared-workbench-build-v1",
|
|
@@ -32,8 +32,8 @@
|
|
|
32
32
|
},
|
|
33
33
|
{
|
|
34
34
|
"path": "package.json",
|
|
35
|
-
"bytes":
|
|
36
|
-
"sha256": "
|
|
35
|
+
"bytes": 1776,
|
|
36
|
+
"sha256": "359e72f8e2c6b348b590151d5c95f8dd9bf08e7812e55baea52c074286072124"
|
|
37
37
|
},
|
|
38
38
|
{
|
|
39
39
|
"path": "plugins/hellolynk-codex/.app.json",
|
|
@@ -43,12 +43,12 @@
|
|
|
43
43
|
{
|
|
44
44
|
"path": "plugins/hellolynk-codex/.codex-plugin/plugin.json",
|
|
45
45
|
"bytes": 1669,
|
|
46
|
-
"sha256": "
|
|
46
|
+
"sha256": "dc33e34ece594590424d3ec1508409e07a02741db5d93d368824e46f12c095a2"
|
|
47
47
|
},
|
|
48
48
|
{
|
|
49
49
|
"path": "plugins/hellolynk-codex/.mcp.json",
|
|
50
|
-
"bytes":
|
|
51
|
-
"sha256": "
|
|
50
|
+
"bytes": 438,
|
|
51
|
+
"sha256": "510ae3b6f417c1cd8e4712704d9e76643d30e7b3d1a8d3363dda19eb30fe4580"
|
|
52
52
|
},
|
|
53
53
|
{
|
|
54
54
|
"path": "plugins/hellolynk-codex/assets/hellolynk-mark.png",
|
|
@@ -58,7 +58,7 @@
|
|
|
58
58
|
{
|
|
59
59
|
"path": "plugins/hellolynk-codex/package.json",
|
|
60
60
|
"bytes": 1030,
|
|
61
|
-
"sha256": "
|
|
61
|
+
"sha256": "be5fc524bab2f659f8f1e851bd1c59c6edf2a5fd4394c1a917a8d5ee9321190e"
|
|
62
62
|
},
|
|
63
63
|
{
|
|
64
64
|
"path": "plugins/hellolynk-codex/runtime/bootstrap/core/hellolynk-client-account.mjs",
|
|
@@ -67,8 +67,8 @@
|
|
|
67
67
|
},
|
|
68
68
|
{
|
|
69
69
|
"path": "plugins/hellolynk-codex/runtime/bootstrap/core/hellolynk-client-core.mjs",
|
|
70
|
-
"bytes":
|
|
71
|
-
"sha256": "
|
|
70
|
+
"bytes": 35808,
|
|
71
|
+
"sha256": "dbab0f067c1a7f02c43b2696e1f5972288acd4f93a18684bf3cc120edff5cf94"
|
|
72
72
|
},
|
|
73
73
|
{
|
|
74
74
|
"path": "plugins/hellolynk-codex/runtime/bootstrap/core/hellolynk-client-profile.mjs",
|
|
@@ -107,14 +107,24 @@
|
|
|
107
107
|
},
|
|
108
108
|
{
|
|
109
109
|
"path": "plugins/hellolynk-codex/runtime/bootstrap/core/hellolynk-platform-credential-store.mjs",
|
|
110
|
-
"bytes":
|
|
111
|
-
"sha256": "
|
|
110
|
+
"bytes": 40123,
|
|
111
|
+
"sha256": "4b4b27cd86f4816e5e6ec4914816a335cdea5c4b4ffb2091775c061a9676a69c"
|
|
112
112
|
},
|
|
113
113
|
{
|
|
114
114
|
"path": "plugins/hellolynk-codex/runtime/bootstrap/core/hellolynk-runtime-safety.cjs",
|
|
115
115
|
"bytes": 24643,
|
|
116
116
|
"sha256": "43d576d9f774409f4a735793c1503a6f397e787b10c59a4ac9cc9257299f5efe"
|
|
117
117
|
},
|
|
118
|
+
{
|
|
119
|
+
"path": "plugins/hellolynk-codex/runtime/bootstrap/platform/darwin/hellolynk-keychain-helper.bin",
|
|
120
|
+
"bytes": 110784,
|
|
121
|
+
"sha256": "f1a782c08229ebc0e8d1704b6390e91c5695e084b4df0a1a6dc043f6b23700ab"
|
|
122
|
+
},
|
|
123
|
+
{
|
|
124
|
+
"path": "plugins/hellolynk-codex/runtime/bootstrap/platform/darwin/hellolynk-keychain-helper.swift",
|
|
125
|
+
"bytes": 7771,
|
|
126
|
+
"sha256": "5d933589a1d739217bc164f73ab9afadc9e27cc512e1c00c0be4b38975fb6d5b"
|
|
127
|
+
},
|
|
118
128
|
{
|
|
119
129
|
"path": "plugins/hellolynk-codex/runtime/bootstrap/platform/windows/hellolynk-host-protected-store.ps1",
|
|
120
130
|
"bytes": 152700,
|
|
@@ -137,8 +147,8 @@
|
|
|
137
147
|
},
|
|
138
148
|
{
|
|
139
149
|
"path": "plugins/hellolynk-codex/runtime/core/hellolynk-client-core.mjs",
|
|
140
|
-
"bytes":
|
|
141
|
-
"sha256": "
|
|
150
|
+
"bytes": 35808,
|
|
151
|
+
"sha256": "dbab0f067c1a7f02c43b2696e1f5972288acd4f93a18684bf3cc120edff5cf94"
|
|
142
152
|
},
|
|
143
153
|
{
|
|
144
154
|
"path": "plugins/hellolynk-codex/runtime/core/hellolynk-client-profile.mjs",
|
|
@@ -322,8 +332,8 @@
|
|
|
322
332
|
},
|
|
323
333
|
{
|
|
324
334
|
"path": "plugins/hellolynk-codex/runtime/core/hellolynk-neutral-host-assembly.mjs",
|
|
325
|
-
"bytes":
|
|
326
|
-
"sha256": "
|
|
335
|
+
"bytes": 46294,
|
|
336
|
+
"sha256": "287ad6e077c8da05799b970ffa6607d8c7a0ab54a4837de51516e6af0ef6695b"
|
|
327
337
|
},
|
|
328
338
|
{
|
|
329
339
|
"path": "plugins/hellolynk-codex/runtime/core/hellolynk-neutral-host-installation-store.mjs",
|
|
@@ -338,7 +348,7 @@
|
|
|
338
348
|
{
|
|
339
349
|
"path": "plugins/hellolynk-codex/runtime/core/hellolynk-neutral-host-process.mjs",
|
|
340
350
|
"bytes": 225161,
|
|
341
|
-
"sha256": "
|
|
351
|
+
"sha256": "ad6809633f49f7316acf6ce5b8f71a13d7ba20d3b62a2700664421ba671b5369"
|
|
342
352
|
},
|
|
343
353
|
{
|
|
344
354
|
"path": "plugins/hellolynk-codex/runtime/core/hellolynk-neutral-host-store-adapters.mjs",
|
|
@@ -352,8 +362,8 @@
|
|
|
352
362
|
},
|
|
353
363
|
{
|
|
354
364
|
"path": "plugins/hellolynk-codex/runtime/core/hellolynk-platform-credential-store.mjs",
|
|
355
|
-
"bytes":
|
|
356
|
-
"sha256": "
|
|
365
|
+
"bytes": 40123,
|
|
366
|
+
"sha256": "4b4b27cd86f4816e5e6ec4914816a335cdea5c4b4ffb2091775c061a9676a69c"
|
|
357
367
|
},
|
|
358
368
|
{
|
|
359
369
|
"path": "plugins/hellolynk-codex/runtime/core/hellolynk-relay-client-cli.mjs",
|
|
@@ -397,13 +407,23 @@
|
|
|
397
407
|
},
|
|
398
408
|
{
|
|
399
409
|
"path": "plugins/hellolynk-codex/runtime/neutral-host-release-manifest.json",
|
|
400
|
-
"bytes":
|
|
401
|
-
"sha256": "
|
|
410
|
+
"bytes": 14698,
|
|
411
|
+
"sha256": "f4b81255068b2a48cdf759b06af6f11409e5e89b8be1de75799c9db053a0a1ec"
|
|
402
412
|
},
|
|
403
413
|
{
|
|
404
414
|
"path": "plugins/hellolynk-codex/runtime/neutral-host-release-pin.json",
|
|
405
415
|
"bytes": 146,
|
|
406
|
-
"sha256": "
|
|
416
|
+
"sha256": "86b4c52c5fa4988c3337698b838fc6f36d468d6342bc7f0075c3e054b29908ee"
|
|
417
|
+
},
|
|
418
|
+
{
|
|
419
|
+
"path": "plugins/hellolynk-codex/runtime/platform/darwin/hellolynk-keychain-helper.bin",
|
|
420
|
+
"bytes": 110784,
|
|
421
|
+
"sha256": "f1a782c08229ebc0e8d1704b6390e91c5695e084b4df0a1a6dc043f6b23700ab"
|
|
422
|
+
},
|
|
423
|
+
{
|
|
424
|
+
"path": "plugins/hellolynk-codex/runtime/platform/darwin/hellolynk-keychain-helper.swift",
|
|
425
|
+
"bytes": 7771,
|
|
426
|
+
"sha256": "5d933589a1d739217bc164f73ab9afadc9e27cc512e1c00c0be4b38975fb6d5b"
|
|
407
427
|
},
|
|
408
428
|
{
|
|
409
429
|
"path": "plugins/hellolynk-codex/runtime/platform/windows/configure-agent.ps1",
|
|
@@ -573,7 +593,7 @@
|
|
|
573
593
|
{
|
|
574
594
|
"path": "plugins/hellolynk-codex/server/codex-neutral-production.mjs",
|
|
575
595
|
"bytes": 27551,
|
|
576
|
-
"sha256": "
|
|
596
|
+
"sha256": "a307100c0b5835f5a6cba5151db367515b7f6cedd09d16af914f28e163df107a"
|
|
577
597
|
},
|
|
578
598
|
{
|
|
579
599
|
"path": "plugins/hellolynk-codex/server/mcp-proxy.mjs",
|
|
@@ -627,8 +647,8 @@
|
|
|
627
647
|
},
|
|
628
648
|
{
|
|
629
649
|
"path": "README.md",
|
|
630
|
-
"bytes":
|
|
631
|
-
"sha256": "
|
|
650
|
+
"bytes": 4578,
|
|
651
|
+
"sha256": "95cd3887026e6a275dce26e50667c1422fad356b59409ce15d6f84ef2207ed92"
|
|
632
652
|
},
|
|
633
653
|
{
|
|
634
654
|
"path": "tools/hellolynk-activate-neutral-host-clients.mjs",
|
package/package.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hellolynk-ai/codex-plugin",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.105-macos.1",
|
|
4
4
|
"description": "HelloLynk Messaging for Codex: open conversations and let your AI message other people's AI assistants.",
|
|
5
5
|
"homepage": "https://hellolynk.com",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|
|
8
8
|
"url": "git+https://github.com/haoyun18881-beep/HelloLynk.git",
|
|
9
|
-
"directory": "clients/codex"
|
|
9
|
+
"directory": "clients/codex-macos"
|
|
10
10
|
},
|
|
11
11
|
"bugs": {
|
|
12
12
|
"url": "https://github.com/haoyun18881-beep/HelloLynk/issues"
|
|
@@ -20,7 +20,10 @@
|
|
|
20
20
|
"license": "SEE LICENSE IN LICENSE",
|
|
21
21
|
"type": "module",
|
|
22
22
|
"os": [
|
|
23
|
-
"
|
|
23
|
+
"darwin"
|
|
24
|
+
],
|
|
25
|
+
"cpu": [
|
|
26
|
+
"arm64"
|
|
24
27
|
],
|
|
25
28
|
"engines": {
|
|
26
29
|
"node": ">=22.16.0"
|
|
@@ -47,7 +50,8 @@
|
|
|
47
50
|
},
|
|
48
51
|
"publishConfig": {
|
|
49
52
|
"access": "public",
|
|
50
|
-
"registry": "https://registry.npmjs.org/"
|
|
53
|
+
"registry": "https://registry.npmjs.org/",
|
|
54
|
+
"tag": "macos"
|
|
51
55
|
},
|
|
52
56
|
"hellolynkClient": {
|
|
53
57
|
"client": "codex",
|
|
@@ -1,19 +1,20 @@
|
|
|
1
|
-
{
|
|
2
|
-
"mcpServers": {
|
|
3
|
-
"hellolynk-codex": {
|
|
4
|
-
"command": "node",
|
|
5
|
-
"args": [
|
|
6
|
-
"./server/mcp-proxy.mjs"
|
|
7
|
-
],
|
|
8
|
-
"cwd": ".",
|
|
9
|
-
"env": {
|
|
10
|
-
"HELLOLYNK_HOST_MODE": "neutral"
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
"
|
|
15
|
-
"
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"mcpServers": {
|
|
3
|
+
"hellolynk-codex": {
|
|
4
|
+
"command": "/Applications/ChatGPT.app/Contents/Resources/cua_node/bin/node",
|
|
5
|
+
"args": [
|
|
6
|
+
"./server/mcp-proxy.mjs"
|
|
7
|
+
],
|
|
8
|
+
"cwd": ".",
|
|
9
|
+
"env": {
|
|
10
|
+
"HELLOLYNK_HOST_MODE": "neutral",
|
|
11
|
+
"TMPDIR": "/tmp"
|
|
12
|
+
},
|
|
13
|
+
"env_vars": [
|
|
14
|
+
"HELLOLYNK_NEUTRAL_STORE_ROOT",
|
|
15
|
+
"CODEX_APP_TOOLS_PIPE_PATH",
|
|
16
|
+
"CODEX_THREAD_ID"
|
|
17
|
+
]
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -1,9 +1,11 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
1
2
|
import runtimeSafety from "./hellolynk-runtime-safety.cjs";
|
|
2
3
|
const DEFAULT_TIMEOUT_MS = 10_000;
|
|
3
4
|
const MAX_JSON_RESPONSE_BYTES = 256 * 1024;
|
|
4
5
|
const LOOPBACK_HOSTS = new Set(["127.0.0.1", "localhost", "[::1]"]);
|
|
5
6
|
const DEVICE_RUNTIME_TTL_MS = 20 * 60 * 1000;
|
|
6
7
|
const DEVICE_RUNTIME_RECOVERY_WINDOW_MS = 120_000;
|
|
8
|
+
const DEVICE_CREDENTIAL_RECHECK_MS = 60 * 60_000;
|
|
7
9
|
const DEVICE_RUNTIME_SCOPES = Object.freeze([
|
|
8
10
|
"relay.text.receive",
|
|
9
11
|
"relay.text.send",
|
|
@@ -567,6 +569,7 @@ export function createDeviceRuntimeSessionManager(options = {}) {
|
|
|
567
569
|
const issue = options.issue || issueDeviceRuntimeSession;
|
|
568
570
|
const now = options.now || (() => Date.now());
|
|
569
571
|
const sessionStore = options.sessionStore || null;
|
|
572
|
+
const credentialRejections = sessionStore?.credentialRejections;
|
|
570
573
|
const sessionKey = options.sessionKey == null ? null : String(options.sessionKey);
|
|
571
574
|
if (sessionStore && (
|
|
572
575
|
!sessionKey ||
|
|
@@ -580,6 +583,8 @@ export function createDeviceRuntimeSessionManager(options = {}) {
|
|
|
580
583
|
5 * 60 * 1000,
|
|
581
584
|
);
|
|
582
585
|
let current = null;
|
|
586
|
+
let rejectedCredential = null;
|
|
587
|
+
let localIssueTail = Promise.resolve();
|
|
583
588
|
|
|
584
589
|
const routeMatches = (session, input) => {
|
|
585
590
|
const expectedValues = [
|
|
@@ -652,22 +657,67 @@ export function createDeviceRuntimeSessionManager(options = {}) {
|
|
|
652
657
|
return proof;
|
|
653
658
|
};
|
|
654
659
|
|
|
660
|
+
const issueSession = async (input, previousSession) => {
|
|
661
|
+
const request = { ...input, previousSession };
|
|
662
|
+
if (options.loadDeviceCredential) request.deviceCredential = await options.loadDeviceCredential();
|
|
663
|
+
const fingerprint = request.deviceCredential && request.baseUrl
|
|
664
|
+
? createHash("sha256").update(JSON.stringify([
|
|
665
|
+
normalizeHelloLynkBaseUrl(request.baseUrl, { allowLoopbackHttp: request.allowLoopbackHttp === true }),
|
|
666
|
+
request.deviceCredential,
|
|
667
|
+
])).digest("hex")
|
|
668
|
+
: null;
|
|
669
|
+
// The production caller holds the existing session lock, including across
|
|
670
|
+
// Host processes. An authority denial may drain the current Host.
|
|
671
|
+
if (fingerprint) {
|
|
672
|
+
const isCoolingDown = (rejection) => {
|
|
673
|
+
if (rejection?.fingerprint !== fingerprint) return false;
|
|
674
|
+
const age = now() - rejection.rejectedAtMs;
|
|
675
|
+
return age >= 0 && age < DEVICE_CREDENTIAL_RECHECK_MS;
|
|
676
|
+
};
|
|
677
|
+
// A generic denial may clear after a server-side repair, without a new
|
|
678
|
+
// credential. The lock permits one recheck once the cooldown ends.
|
|
679
|
+
if (isCoolingDown(rejectedCredential)
|
|
680
|
+
|| isCoolingDown(await credentialRejections?.load(sessionKey).catch(() => null))) {
|
|
681
|
+
throw new HelloLynkClientError("device_credential_invalid", { httpStatus: 401 });
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
let session;
|
|
685
|
+
try {
|
|
686
|
+
session = reusable(await issue(request), input);
|
|
687
|
+
} catch (error) {
|
|
688
|
+
if (fingerprint && error?.httpStatus === 401 && error.code === "device_credential_invalid") {
|
|
689
|
+
rejectedCredential = { fingerprint, rejectedAtMs: now() };
|
|
690
|
+
// Preserve the authority denial even if local storage is unavailable.
|
|
691
|
+
await credentialRejections?.save(sessionKey, rejectedCredential).catch(() => {});
|
|
692
|
+
}
|
|
693
|
+
throw error;
|
|
694
|
+
}
|
|
695
|
+
if (session && fingerprint) {
|
|
696
|
+
rejectedCredential = null;
|
|
697
|
+
await credentialRejections?.remove(sessionKey).catch(() => {});
|
|
698
|
+
}
|
|
699
|
+
return session;
|
|
700
|
+
};
|
|
701
|
+
|
|
655
702
|
return {
|
|
656
703
|
async get(input) {
|
|
657
704
|
if (!sessionStore) {
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
705
|
+
const result = localIssueTail.then(async () => {
|
|
706
|
+
if (
|
|
707
|
+
current && current.expiresAtMs - now() > minimumRemainingMs
|
|
708
|
+
&& routeMatches(current, input)
|
|
709
|
+
&& scopesMatch(current, input)
|
|
710
|
+
&& (input?.expectedAgentId == null || input.expectedAgentId === current.agentId)
|
|
711
|
+
&& (input?.expectedPrincipalId == null || input.expectedPrincipalId === current.principalId)
|
|
712
|
+
) return current;
|
|
713
|
+
const reusableCurrent = reusable(current, input);
|
|
714
|
+
if (reusableCurrent) return reusableCurrent;
|
|
715
|
+
current = await issueSession(input, recoveryProof(current, input));
|
|
716
|
+
if (!current) throw new HelloLynkClientError("invalid_runtime_session_handle");
|
|
717
|
+
return current;
|
|
718
|
+
});
|
|
719
|
+
localIssueTail = result.catch(() => {});
|
|
720
|
+
return result;
|
|
671
721
|
}
|
|
672
722
|
const inMemory = reusable(current, input);
|
|
673
723
|
if (inMemory) return inMemory;
|
|
@@ -678,7 +728,7 @@ export function createDeviceRuntimeSessionManager(options = {}) {
|
|
|
678
728
|
current = persisted;
|
|
679
729
|
return current;
|
|
680
730
|
}
|
|
681
|
-
const issued =
|
|
731
|
+
const issued = await issueSession(input, persisted);
|
|
682
732
|
if (!issued) throw new HelloLynkClientError("invalid_runtime_session_handle");
|
|
683
733
|
await sessionStore.save(sessionKey, issued);
|
|
684
734
|
current = issued;
|
package/plugins/hellolynk-codex/runtime/bootstrap/core/hellolynk-platform-credential-store.mjs
CHANGED
|
@@ -297,6 +297,36 @@ export async function secureWindowsClientReleaseTree(clientRoot, releaseId, opti
|
|
|
297
297
|
});
|
|
298
298
|
}
|
|
299
299
|
|
|
300
|
+
// Accessed under the same cross-process lock as the corresponding session.
|
|
301
|
+
function createCredentialRejectionStore(root) {
|
|
302
|
+
const fileFor = (profile) => join(root, `runtime-session-rejection-${assertCredentialProfileId(profile)}.json`);
|
|
303
|
+
const isRejection = (value) => typeof value?.fingerprint === "string" && /^[a-f0-9]{64}$/.test(value.fingerprint)
|
|
304
|
+
&& Number.isSafeInteger(value.rejectedAtMs) && value.rejectedAtMs >= 0;
|
|
305
|
+
return {
|
|
306
|
+
async load(profile) {
|
|
307
|
+
let value;
|
|
308
|
+
try { value = JSON.parse(await readFile(fileFor(profile), "utf8")); }
|
|
309
|
+
catch (error) { if (error?.code === "ENOENT" || error instanceof SyntaxError) return null; throw error; }
|
|
310
|
+
return isRejection(value) ? value : null;
|
|
311
|
+
},
|
|
312
|
+
async save(profile, rejection) {
|
|
313
|
+
if (!isRejection(rejection)) throw new HelloLynkCredentialStoreError("invalid_credential_rejection");
|
|
314
|
+
const { fingerprint, rejectedAtMs } = rejection;
|
|
315
|
+
const file = fileFor(profile);
|
|
316
|
+
const temporary = `${file}.${randomBytes(12).toString("hex")}.tmp`;
|
|
317
|
+
try {
|
|
318
|
+
await writeFile(temporary, `${JSON.stringify({ fingerprint, rejectedAtMs })}\n`, { encoding: "utf8", flag: "wx", mode: 0o600 });
|
|
319
|
+
await rename(temporary, file);
|
|
320
|
+
} finally {
|
|
321
|
+
await rm(temporary, { force: true }).catch(() => {});
|
|
322
|
+
}
|
|
323
|
+
},
|
|
324
|
+
async remove(profile) {
|
|
325
|
+
await rm(fileFor(profile), { force: true });
|
|
326
|
+
},
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
|
|
300
330
|
export function createPlatformCredentialStore(options = {}) {
|
|
301
331
|
const platform = options.platform || process.platform;
|
|
302
332
|
const run = options.runHelper || runCredentialHelper;
|
|
@@ -370,6 +400,7 @@ export function createPlatformCredentialStore(options = {}) {
|
|
|
370
400
|
await invoke("Remove", assertCredentialProfileId(profile), "");
|
|
371
401
|
},
|
|
372
402
|
runtimeSessions: {
|
|
403
|
+
credentialRejections: createCredentialRejectionStore(runtimeStoreRoot),
|
|
373
404
|
async save(profile, session) {
|
|
374
405
|
const body = JSON.stringify({ schemaVersion: 1, ...assertRuntimeSession(session) });
|
|
375
406
|
await invoke("StoreRuntimeSession", assertCredentialProfileId(profile), body);
|
|
@@ -504,6 +535,7 @@ export function createPlatformCredentialStore(options = {}) {
|
|
|
504
535
|
},
|
|
505
536
|
async remove(profile) { await entry(root, "device", profile, "remove"); },
|
|
506
537
|
runtimeSessions: {
|
|
538
|
+
credentialRejections: createCredentialRejectionStore(root),
|
|
507
539
|
async save(profile, session) { await entry(root, "session", profile, "save", assertRuntimeSession(session)); },
|
|
508
540
|
async load(profile) {
|
|
509
541
|
const value = await entry(root, "session", profile, "load");
|
package/plugins/hellolynk-codex/runtime/bootstrap/platform/darwin/hellolynk-keychain-helper.bin
ADDED
|
Binary file
|
package/plugins/hellolynk-codex/runtime/bootstrap/platform/darwin/hellolynk-keychain-helper.swift
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
// Rebuild in this directory: xcrun swiftc -O -target arm64-apple-macos13.0 -module-name HelloLynkKeychain hellolynk-keychain-helper.swift -o hellolynk-keychain-helper.bin
|
|
2
|
+
import AppKit
|
|
3
|
+
import CryptoKit
|
|
4
|
+
import Foundation
|
|
5
|
+
import Security
|
|
6
|
+
|
|
7
|
+
func fail(_ code: String) -> Never {
|
|
8
|
+
FileHandle.standardError.write(Data((code + "\n").utf8))
|
|
9
|
+
exit(1)
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
func requireNoGrantACL(_ path: String) {
|
|
13
|
+
guard let acl = acl_get_file(path, ACL_TYPE_EXTENDED) else {
|
|
14
|
+
if errno == ENOENT { return } // A first write creates the file with mode 0600.
|
|
15
|
+
fail("macos_vault_acl_unavailable")
|
|
16
|
+
}
|
|
17
|
+
defer { acl_free(UnsafeMutableRawPointer(acl)) }
|
|
18
|
+
var entry: acl_entry_t?
|
|
19
|
+
var selector = ACL_FIRST_ENTRY
|
|
20
|
+
while acl_get_entry(acl, Int32(selector.rawValue), &entry) == 0 {
|
|
21
|
+
var tag = ACL_UNDEFINED_TAG
|
|
22
|
+
guard acl_get_tag_type(entry, &tag) == 0, tag == ACL_EXTENDED_DENY else {
|
|
23
|
+
fail("macos_vault_acl_invalid")
|
|
24
|
+
}
|
|
25
|
+
selector = ACL_NEXT_ENTRY
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// The caller verifies the release path's ancestors and package hashes. Walk
|
|
30
|
+
// with lstat so a symlink or hard link cannot redirect a permission change.
|
|
31
|
+
func secureRelease(_ path: String, verifyOnly: Bool) {
|
|
32
|
+
var pending = [path]
|
|
33
|
+
while let entry = pending.popLast() {
|
|
34
|
+
var st = stat()
|
|
35
|
+
guard lstat(entry, &st) == 0, st.st_uid == getuid() else { fail("macos_vault_release_invalid") }
|
|
36
|
+
let type = st.st_mode & S_IFMT
|
|
37
|
+
guard type == S_IFDIR || (type == S_IFREG && st.st_nlink == 1) else { fail("macos_vault_release_invalid") }
|
|
38
|
+
let mode: mode_t = type == S_IFDIR || entry.hasSuffix(".bin") ? 0o700 : 0o600
|
|
39
|
+
if (st.st_mode & 0o7777) != mode {
|
|
40
|
+
guard !verifyOnly, chmod(entry, mode) == 0 else { fail("macos_vault_release_permissions_invalid") }
|
|
41
|
+
}
|
|
42
|
+
requireNoGrantACL(entry)
|
|
43
|
+
if type == S_IFDIR {
|
|
44
|
+
guard let names = try? FileManager.default.contentsOfDirectory(atPath: entry) else { fail("macos_vault_release_invalid") }
|
|
45
|
+
pending.append(contentsOf: names.map { entry + "/" + $0 })
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
func masterKey(_ root: String, create: Bool) -> Data {
|
|
51
|
+
let account = SHA256.hash(data: Data(root.utf8)).map { String(format: "%02x", $0) }.joined()
|
|
52
|
+
let base: [String: Any] = [
|
|
53
|
+
kSecClass as String: kSecClassGenericPassword,
|
|
54
|
+
kSecAttrService as String: "com.hellolynk.codex.macos-vault.v1",
|
|
55
|
+
kSecAttrAccount as String: account
|
|
56
|
+
]
|
|
57
|
+
var lookup = base
|
|
58
|
+
lookup[kSecReturnData as String] = true
|
|
59
|
+
lookup[kSecMatchLimit as String] = kSecMatchLimitOne
|
|
60
|
+
var result: CFTypeRef?
|
|
61
|
+
let status = SecItemCopyMatching(lookup as CFDictionary, &result)
|
|
62
|
+
if status == errSecSuccess {
|
|
63
|
+
guard let key = result as? Data, key.count == 32 else { fail("macos_vault_key_invalid") }
|
|
64
|
+
return key
|
|
65
|
+
}
|
|
66
|
+
guard status == errSecItemNotFound else { fail("macos_keychain_access_\(abs(Int(status)))") }
|
|
67
|
+
guard create else { fail("macos_vault_key_missing") }
|
|
68
|
+
let key = SymmetricKey(size: .bits256).withUnsafeBytes { Data($0) }
|
|
69
|
+
var add = base
|
|
70
|
+
add[kSecValueData as String] = key
|
|
71
|
+
add[kSecAttrAccessible as String] = kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly
|
|
72
|
+
let inserted = SecItemAdd(add as CFDictionary, nil)
|
|
73
|
+
if inserted == errSecDuplicateItem { return masterKey(root, create: false) }
|
|
74
|
+
guard inserted == errSecSuccess else { fail("macos_keychain_access_\(abs(Int(inserted)))") }
|
|
75
|
+
return key
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
func nativeConfirmation() {
|
|
79
|
+
let raw = FileHandle.standardInput.readDataToEndOfFile()
|
|
80
|
+
guard raw.count <= 20480 * 6 + 8192,
|
|
81
|
+
var v = (try? JSONSerialization.jsonObject(with: raw)) as? [String: Any],
|
|
82
|
+
let body = v["displayText"] as? String, !body.isEmpty, body.utf8.count <= 20480,
|
|
83
|
+
let encoded = v["hmacKey"] as? String, encoded.count == 43,
|
|
84
|
+
let key = Data(base64Encoded: encoded.replacingOccurrences(of: "-", with: "+")
|
|
85
|
+
.replacingOccurrences(of: "_", with: "/") + "="), key.count == 32,
|
|
86
|
+
let recipient = v["recipientHelloLynkId"] as? String,
|
|
87
|
+
v["contract"] as? String == "hellolynk-native-human-confirmation-v1",
|
|
88
|
+
v["textDigest"] as? String == SHA256.hash(data: Data(body.utf8)).map({ String(format: "%02x", $0) }).joined()
|
|
89
|
+
else { fail("human_confirmation_request_invalid") }
|
|
90
|
+
|
|
91
|
+
var fields = ["contract", "actionId", "decision", "principalId", "delegateId", "hostInstallationId",
|
|
92
|
+
"adapterId", "adapterInstanceId", "hostGeneration", "recipientHelloLynkId"]
|
|
93
|
+
var label = "收件人:" + recipient
|
|
94
|
+
if v["recipientDelegateId"] != nil || v["recipientInstanceDisplayName"] != nil {
|
|
95
|
+
guard let name = v["recipientInstanceDisplayName"] as? String, !name.isEmpty, name.utf8.count <= 512,
|
|
96
|
+
name.range(of: #"[\x00-\x1F\x7F-\x9F\u2028\u2029\u202A-\u202E\u2066-\u2069]"#, options: .regularExpression) == nil,
|
|
97
|
+
v["recipientDelegateId"] is String else { fail("human_confirmation_request_invalid") }
|
|
98
|
+
fields += ["recipientDelegateId", "recipientInstanceDisplayName"]
|
|
99
|
+
label += "\n目标 AI:" + name
|
|
100
|
+
}
|
|
101
|
+
fields += ["clientMessageId", "textDigest", "createdAtMs", "expiresAtMs", "challenge"]
|
|
102
|
+
var expected = Set(fields)
|
|
103
|
+
expected.remove("decision")
|
|
104
|
+
expected.formUnion(["displayText", "hmacKey"])
|
|
105
|
+
guard expected == Set(v.keys) else { fail("human_confirmation_request_invalid") }
|
|
106
|
+
let app = NSApplication.shared
|
|
107
|
+
app.setActivationPolicy(.accessory)
|
|
108
|
+
app.activate(ignoringOtherApps: true)
|
|
109
|
+
let dialog = NSAlert()
|
|
110
|
+
dialog.messageText = "是否允许 HelloLynk 发送以下文本?"
|
|
111
|
+
dialog.informativeText = label
|
|
112
|
+
dialog.addButton(withTitle: "取消")
|
|
113
|
+
dialog.addButton(withTitle: "批准发送")
|
|
114
|
+
let scroll = NSScrollView(frame: NSRect(x: 0, y: 0, width: 500, height: 230))
|
|
115
|
+
scroll.hasVerticalScroller = true
|
|
116
|
+
let view = NSTextView(frame: scroll.contentView.bounds)
|
|
117
|
+
view.isEditable = false
|
|
118
|
+
view.isRichText = false
|
|
119
|
+
view.isVerticallyResizable = true
|
|
120
|
+
view.autoresizingMask = [.width]
|
|
121
|
+
view.textContainer?.widthTracksTextView = true
|
|
122
|
+
view.string = body
|
|
123
|
+
scroll.documentView = view
|
|
124
|
+
dialog.accessoryView = scroll
|
|
125
|
+
v["decision"] = dialog.runModal() == .alertSecondButtonReturn ? "approve" : "cancel"
|
|
126
|
+
let message = fields.map { field -> String in
|
|
127
|
+
if let text = v[field] as? String { return text }
|
|
128
|
+
if ["createdAtMs","expiresAtMs"].contains(field), let n = v[field] as? NSNumber { return n.stringValue }
|
|
129
|
+
fail("human_confirmation_request_invalid")
|
|
130
|
+
}.joined(separator: "\n")
|
|
131
|
+
let proof = HMAC<SHA256>.authenticationCode(for: Data(message.utf8), using: SymmetricKey(data: key))
|
|
132
|
+
v["proof"] = proof.map { String(format: "%02x", $0) }.joined()
|
|
133
|
+
for field in ["hmacKey", "challenge", "displayText"] { v.removeValue(forKey: field) }
|
|
134
|
+
guard let output = try? JSONSerialization.data(withJSONObject: v) else { fail("human_confirmation_response_invalid") }
|
|
135
|
+
FileHandle.standardOutput.write(output)
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
let args = Array(CommandLine.arguments.dropFirst())
|
|
139
|
+
if args == ["confirm"] { nativeConfirmation() }
|
|
140
|
+
else if args.count == 3, args[0] == "key", args[1].hasPrefix("/"), ["load","create"].contains(args[2]) {
|
|
141
|
+
requireNoGrantACL(args[1])
|
|
142
|
+
requireNoGrantACL(args[1] + "/protected-v1.sqlite")
|
|
143
|
+
let key = masterKey(args[1], create: args[2] == "create")
|
|
144
|
+
FileHandle(fileDescriptor: 3, closeOnDealloc: false).write(key)
|
|
145
|
+
} else if args.count == 2, ["verify-tree", "secure-tree"].contains(args[0]), args[1].hasPrefix("/") {
|
|
146
|
+
secureRelease(args[1], verifyOnly: args[0] == "verify-tree")
|
|
147
|
+
} else if args.count == 2, args[0] == "verify-acl", args[1].hasPrefix("/") {
|
|
148
|
+
requireNoGrantACL(args[1])
|
|
149
|
+
} else { fail("macos_vault_helper_arguments_invalid") }
|
|
@@ -1,9 +1,11 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
1
2
|
import runtimeSafety from "./hellolynk-runtime-safety.cjs";
|
|
2
3
|
const DEFAULT_TIMEOUT_MS = 10_000;
|
|
3
4
|
const MAX_JSON_RESPONSE_BYTES = 256 * 1024;
|
|
4
5
|
const LOOPBACK_HOSTS = new Set(["127.0.0.1", "localhost", "[::1]"]);
|
|
5
6
|
const DEVICE_RUNTIME_TTL_MS = 20 * 60 * 1000;
|
|
6
7
|
const DEVICE_RUNTIME_RECOVERY_WINDOW_MS = 120_000;
|
|
8
|
+
const DEVICE_CREDENTIAL_RECHECK_MS = 60 * 60_000;
|
|
7
9
|
const DEVICE_RUNTIME_SCOPES = Object.freeze([
|
|
8
10
|
"relay.text.receive",
|
|
9
11
|
"relay.text.send",
|
|
@@ -567,6 +569,7 @@ export function createDeviceRuntimeSessionManager(options = {}) {
|
|
|
567
569
|
const issue = options.issue || issueDeviceRuntimeSession;
|
|
568
570
|
const now = options.now || (() => Date.now());
|
|
569
571
|
const sessionStore = options.sessionStore || null;
|
|
572
|
+
const credentialRejections = sessionStore?.credentialRejections;
|
|
570
573
|
const sessionKey = options.sessionKey == null ? null : String(options.sessionKey);
|
|
571
574
|
if (sessionStore && (
|
|
572
575
|
!sessionKey ||
|
|
@@ -580,6 +583,8 @@ export function createDeviceRuntimeSessionManager(options = {}) {
|
|
|
580
583
|
5 * 60 * 1000,
|
|
581
584
|
);
|
|
582
585
|
let current = null;
|
|
586
|
+
let rejectedCredential = null;
|
|
587
|
+
let localIssueTail = Promise.resolve();
|
|
583
588
|
|
|
584
589
|
const routeMatches = (session, input) => {
|
|
585
590
|
const expectedValues = [
|
|
@@ -652,22 +657,67 @@ export function createDeviceRuntimeSessionManager(options = {}) {
|
|
|
652
657
|
return proof;
|
|
653
658
|
};
|
|
654
659
|
|
|
660
|
+
const issueSession = async (input, previousSession) => {
|
|
661
|
+
const request = { ...input, previousSession };
|
|
662
|
+
if (options.loadDeviceCredential) request.deviceCredential = await options.loadDeviceCredential();
|
|
663
|
+
const fingerprint = request.deviceCredential && request.baseUrl
|
|
664
|
+
? createHash("sha256").update(JSON.stringify([
|
|
665
|
+
normalizeHelloLynkBaseUrl(request.baseUrl, { allowLoopbackHttp: request.allowLoopbackHttp === true }),
|
|
666
|
+
request.deviceCredential,
|
|
667
|
+
])).digest("hex")
|
|
668
|
+
: null;
|
|
669
|
+
// The production caller holds the existing session lock, including across
|
|
670
|
+
// Host processes. An authority denial may drain the current Host.
|
|
671
|
+
if (fingerprint) {
|
|
672
|
+
const isCoolingDown = (rejection) => {
|
|
673
|
+
if (rejection?.fingerprint !== fingerprint) return false;
|
|
674
|
+
const age = now() - rejection.rejectedAtMs;
|
|
675
|
+
return age >= 0 && age < DEVICE_CREDENTIAL_RECHECK_MS;
|
|
676
|
+
};
|
|
677
|
+
// A generic denial may clear after a server-side repair, without a new
|
|
678
|
+
// credential. The lock permits one recheck once the cooldown ends.
|
|
679
|
+
if (isCoolingDown(rejectedCredential)
|
|
680
|
+
|| isCoolingDown(await credentialRejections?.load(sessionKey).catch(() => null))) {
|
|
681
|
+
throw new HelloLynkClientError("device_credential_invalid", { httpStatus: 401 });
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
let session;
|
|
685
|
+
try {
|
|
686
|
+
session = reusable(await issue(request), input);
|
|
687
|
+
} catch (error) {
|
|
688
|
+
if (fingerprint && error?.httpStatus === 401 && error.code === "device_credential_invalid") {
|
|
689
|
+
rejectedCredential = { fingerprint, rejectedAtMs: now() };
|
|
690
|
+
// Preserve the authority denial even if local storage is unavailable.
|
|
691
|
+
await credentialRejections?.save(sessionKey, rejectedCredential).catch(() => {});
|
|
692
|
+
}
|
|
693
|
+
throw error;
|
|
694
|
+
}
|
|
695
|
+
if (session && fingerprint) {
|
|
696
|
+
rejectedCredential = null;
|
|
697
|
+
await credentialRejections?.remove(sessionKey).catch(() => {});
|
|
698
|
+
}
|
|
699
|
+
return session;
|
|
700
|
+
};
|
|
701
|
+
|
|
655
702
|
return {
|
|
656
703
|
async get(input) {
|
|
657
704
|
if (!sessionStore) {
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
705
|
+
const result = localIssueTail.then(async () => {
|
|
706
|
+
if (
|
|
707
|
+
current && current.expiresAtMs - now() > minimumRemainingMs
|
|
708
|
+
&& routeMatches(current, input)
|
|
709
|
+
&& scopesMatch(current, input)
|
|
710
|
+
&& (input?.expectedAgentId == null || input.expectedAgentId === current.agentId)
|
|
711
|
+
&& (input?.expectedPrincipalId == null || input.expectedPrincipalId === current.principalId)
|
|
712
|
+
) return current;
|
|
713
|
+
const reusableCurrent = reusable(current, input);
|
|
714
|
+
if (reusableCurrent) return reusableCurrent;
|
|
715
|
+
current = await issueSession(input, recoveryProof(current, input));
|
|
716
|
+
if (!current) throw new HelloLynkClientError("invalid_runtime_session_handle");
|
|
717
|
+
return current;
|
|
718
|
+
});
|
|
719
|
+
localIssueTail = result.catch(() => {});
|
|
720
|
+
return result;
|
|
671
721
|
}
|
|
672
722
|
const inMemory = reusable(current, input);
|
|
673
723
|
if (inMemory) return inMemory;
|
|
@@ -678,7 +728,7 @@ export function createDeviceRuntimeSessionManager(options = {}) {
|
|
|
678
728
|
current = persisted;
|
|
679
729
|
return current;
|
|
680
730
|
}
|
|
681
|
-
const issued =
|
|
731
|
+
const issued = await issueSession(input, persisted);
|
|
682
732
|
if (!issued) throw new HelloLynkClientError("invalid_runtime_session_handle");
|
|
683
733
|
await sessionStore.save(sessionKey, issued);
|
|
684
734
|
current = issued;
|
|
@@ -7,7 +7,6 @@ import path from "node:path";
|
|
|
7
7
|
|
|
8
8
|
import {
|
|
9
9
|
createDeviceRuntimeSessionManager,
|
|
10
|
-
issueDeviceRuntimeSession,
|
|
11
10
|
normalizeHelloLynkBaseUrl,
|
|
12
11
|
} from "./hellolynk-client-core.mjs";
|
|
13
12
|
import { readClientProfile } from "./hellolynk-client-profile.mjs";
|
|
@@ -243,31 +242,19 @@ export async function startHelloLynkNeutralHostAssembly(options = {}, dependenci
|
|
|
243
242
|
const manager = (dependencies.createRuntimeSessionManager || createDeviceRuntimeSessionManager)({
|
|
244
243
|
sessionStore: credentialStore.runtimeSessions,
|
|
245
244
|
sessionKey,
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
try {
|
|
249
|
-
return await (dependencies.issueRuntimeSession || issueDeviceRuntimeSession)({
|
|
250
|
-
baseUrl: profile.baseUrl,
|
|
251
|
-
allowLoopbackHttp: profile.allowLoopbackHttp,
|
|
252
|
-
deviceCredential,
|
|
253
|
-
expectedPrincipalId: profile.principalId,
|
|
254
|
-
expectedAgentId: profile.agentId,
|
|
255
|
-
expectedDelegateId: profile.delegateId,
|
|
256
|
-
expectedHostId: profile.hostId,
|
|
257
|
-
expectedAuthorizationVersion: profile.authorizationVersion,
|
|
258
|
-
previousSession,
|
|
259
|
-
fetchImpl: dependencies.fetchImpl,
|
|
260
|
-
});
|
|
261
|
-
} finally { deviceCredential = null; }
|
|
262
|
-
},
|
|
245
|
+
loadDeviceCredential: () => credentialStore.load(profile.profileId),
|
|
246
|
+
issue: dependencies.issueRuntimeSession,
|
|
263
247
|
});
|
|
264
248
|
const sessionProvider = Object.freeze({
|
|
265
249
|
get: () => manager.get({
|
|
250
|
+
baseUrl: profile.baseUrl,
|
|
251
|
+
allowLoopbackHttp: profile.allowLoopbackHttp,
|
|
266
252
|
expectedPrincipalId: profile.principalId,
|
|
267
253
|
expectedAgentId: profile.agentId,
|
|
268
254
|
expectedDelegateId: profile.delegateId,
|
|
269
255
|
expectedHostId: profile.hostId,
|
|
270
256
|
expectedAuthorizationVersion: profile.authorizationVersion,
|
|
257
|
+
fetchImpl: dependencies.fetchImpl,
|
|
271
258
|
}),
|
|
272
259
|
clear: (error) => manager.clear(error),
|
|
273
260
|
status: () => manager.status(),
|
|
@@ -34,7 +34,7 @@ export const HELLOLYNK_NEUTRAL_HOST_PROCESS_CONTRACT = "hellolynk-neutral-host-p
|
|
|
34
34
|
export const HELLOLYNK_NEUTRAL_HOST_DISCOVERY_CONTRACT = "hellolynk-neutral-host-discovery-v1";
|
|
35
35
|
// Raised with every release batch, together with the six client versions. A Host reports it
|
|
36
36
|
// in its health; the higher release stays and the lower one makes room.
|
|
37
|
-
export const HELLOLYNK_HOST_RELEASE_ORDINAL =
|
|
37
|
+
export const HELLOLYNK_HOST_RELEASE_ORDINAL = 17;
|
|
38
38
|
|
|
39
39
|
const INSTALLATION_LOCK_CONTRACT = "hellolynk-neutral-host-installation-lock-v2";
|
|
40
40
|
const INSTALLATION_LOCK_MAX_BYTES = 4 * 1024;
|
|
@@ -297,6 +297,36 @@ export async function secureWindowsClientReleaseTree(clientRoot, releaseId, opti
|
|
|
297
297
|
});
|
|
298
298
|
}
|
|
299
299
|
|
|
300
|
+
// Accessed under the same cross-process lock as the corresponding session.
|
|
301
|
+
function createCredentialRejectionStore(root) {
|
|
302
|
+
const fileFor = (profile) => join(root, `runtime-session-rejection-${assertCredentialProfileId(profile)}.json`);
|
|
303
|
+
const isRejection = (value) => typeof value?.fingerprint === "string" && /^[a-f0-9]{64}$/.test(value.fingerprint)
|
|
304
|
+
&& Number.isSafeInteger(value.rejectedAtMs) && value.rejectedAtMs >= 0;
|
|
305
|
+
return {
|
|
306
|
+
async load(profile) {
|
|
307
|
+
let value;
|
|
308
|
+
try { value = JSON.parse(await readFile(fileFor(profile), "utf8")); }
|
|
309
|
+
catch (error) { if (error?.code === "ENOENT" || error instanceof SyntaxError) return null; throw error; }
|
|
310
|
+
return isRejection(value) ? value : null;
|
|
311
|
+
},
|
|
312
|
+
async save(profile, rejection) {
|
|
313
|
+
if (!isRejection(rejection)) throw new HelloLynkCredentialStoreError("invalid_credential_rejection");
|
|
314
|
+
const { fingerprint, rejectedAtMs } = rejection;
|
|
315
|
+
const file = fileFor(profile);
|
|
316
|
+
const temporary = `${file}.${randomBytes(12).toString("hex")}.tmp`;
|
|
317
|
+
try {
|
|
318
|
+
await writeFile(temporary, `${JSON.stringify({ fingerprint, rejectedAtMs })}\n`, { encoding: "utf8", flag: "wx", mode: 0o600 });
|
|
319
|
+
await rename(temporary, file);
|
|
320
|
+
} finally {
|
|
321
|
+
await rm(temporary, { force: true }).catch(() => {});
|
|
322
|
+
}
|
|
323
|
+
},
|
|
324
|
+
async remove(profile) {
|
|
325
|
+
await rm(fileFor(profile), { force: true });
|
|
326
|
+
},
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
|
|
300
330
|
export function createPlatformCredentialStore(options = {}) {
|
|
301
331
|
const platform = options.platform || process.platform;
|
|
302
332
|
const run = options.runHelper || runCredentialHelper;
|
|
@@ -370,6 +400,7 @@ export function createPlatformCredentialStore(options = {}) {
|
|
|
370
400
|
await invoke("Remove", assertCredentialProfileId(profile), "");
|
|
371
401
|
},
|
|
372
402
|
runtimeSessions: {
|
|
403
|
+
credentialRejections: createCredentialRejectionStore(runtimeStoreRoot),
|
|
373
404
|
async save(profile, session) {
|
|
374
405
|
const body = JSON.stringify({ schemaVersion: 1, ...assertRuntimeSession(session) });
|
|
375
406
|
await invoke("StoreRuntimeSession", assertCredentialProfileId(profile), body);
|
|
@@ -504,6 +535,7 @@ export function createPlatformCredentialStore(options = {}) {
|
|
|
504
535
|
},
|
|
505
536
|
async remove(profile) { await entry(root, "device", profile, "remove"); },
|
|
506
537
|
runtimeSessions: {
|
|
538
|
+
credentialRejections: createCredentialRejectionStore(root),
|
|
507
539
|
async save(profile, session) { await entry(root, "session", profile, "save", assertRuntimeSession(session)); },
|
|
508
540
|
async load(profile) {
|
|
509
541
|
const value = await entry(root, "session", profile, "load");
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
"files": [
|
|
5
5
|
{
|
|
6
6
|
"path": "release/package.json",
|
|
7
|
-
"sha256": "
|
|
7
|
+
"sha256": "be5fc524bab2f659f8f1e851bd1c59c6edf2a5fd4394c1a917a8d5ee9321190e"
|
|
8
8
|
},
|
|
9
9
|
{
|
|
10
10
|
"path": "neutral-host-bootstrap.mjs",
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
},
|
|
33
33
|
{
|
|
34
34
|
"path": "core/hellolynk-client-core.mjs",
|
|
35
|
-
"sha256": "
|
|
35
|
+
"sha256": "dbab0f067c1a7f02c43b2696e1f5972288acd4f93a18684bf3cc120edff5cf94"
|
|
36
36
|
},
|
|
37
37
|
{
|
|
38
38
|
"path": "core/hellolynk-client-profile.mjs",
|
|
@@ -184,7 +184,7 @@
|
|
|
184
184
|
},
|
|
185
185
|
{
|
|
186
186
|
"path": "core/hellolynk-neutral-host-assembly.mjs",
|
|
187
|
-
"sha256": "
|
|
187
|
+
"sha256": "287ad6e077c8da05799b970ffa6607d8c7a0ab54a4837de51516e6af0ef6695b"
|
|
188
188
|
},
|
|
189
189
|
{
|
|
190
190
|
"path": "core/hellolynk-neutral-host-installation-store.mjs",
|
|
@@ -196,7 +196,7 @@
|
|
|
196
196
|
},
|
|
197
197
|
{
|
|
198
198
|
"path": "core/hellolynk-neutral-host-process.mjs",
|
|
199
|
-
"sha256": "
|
|
199
|
+
"sha256": "ad6809633f49f7316acf6ce5b8f71a13d7ba20d3b62a2700664421ba671b5369"
|
|
200
200
|
},
|
|
201
201
|
{
|
|
202
202
|
"path": "core/hellolynk-neutral-host-store-adapters.mjs",
|
|
@@ -208,7 +208,7 @@
|
|
|
208
208
|
},
|
|
209
209
|
{
|
|
210
210
|
"path": "core/hellolynk-platform-credential-store.mjs",
|
|
211
|
-
"sha256": "
|
|
211
|
+
"sha256": "4b4b27cd86f4816e5e6ec4914816a335cdea5c4b4ffb2091775c061a9676a69c"
|
|
212
212
|
},
|
|
213
213
|
{
|
|
214
214
|
"path": "core/hellolynk-relay-client-cli.mjs",
|
|
@@ -324,7 +324,7 @@
|
|
|
324
324
|
},
|
|
325
325
|
{
|
|
326
326
|
"path": "bootstrap/core/hellolynk-client-core.mjs",
|
|
327
|
-
"sha256": "
|
|
327
|
+
"sha256": "dbab0f067c1a7f02c43b2696e1f5972288acd4f93a18684bf3cc120edff5cf94"
|
|
328
328
|
},
|
|
329
329
|
{
|
|
330
330
|
"path": "bootstrap/core/hellolynk-client-profile.mjs",
|
|
@@ -348,7 +348,7 @@
|
|
|
348
348
|
},
|
|
349
349
|
{
|
|
350
350
|
"path": "bootstrap/core/hellolynk-platform-credential-store.mjs",
|
|
351
|
-
"sha256": "
|
|
351
|
+
"sha256": "4b4b27cd86f4816e5e6ec4914816a335cdea5c4b4ffb2091775c061a9676a69c"
|
|
352
352
|
},
|
|
353
353
|
{
|
|
354
354
|
"path": "bootstrap/core/hellolynk-runtime-safety.cjs",
|
|
@@ -358,9 +358,25 @@
|
|
|
358
358
|
"path": "bootstrap/platform/windows/hellolynk-host-protected-store.ps1",
|
|
359
359
|
"sha256": "24357e8ce54090b4ec68e457f02c9e834a80f3273ef3c03a8c0fa7258797fb49"
|
|
360
360
|
},
|
|
361
|
+
{
|
|
362
|
+
"path": "platform/darwin/hellolynk-keychain-helper.swift",
|
|
363
|
+
"sha256": "5d933589a1d739217bc164f73ab9afadc9e27cc512e1c00c0be4b38975fb6d5b"
|
|
364
|
+
},
|
|
365
|
+
{
|
|
366
|
+
"path": "platform/darwin/hellolynk-keychain-helper.bin",
|
|
367
|
+
"sha256": "f1a782c08229ebc0e8d1704b6390e91c5695e084b4df0a1a6dc043f6b23700ab"
|
|
368
|
+
},
|
|
369
|
+
{
|
|
370
|
+
"path": "bootstrap/platform/darwin/hellolynk-keychain-helper.swift",
|
|
371
|
+
"sha256": "5d933589a1d739217bc164f73ab9afadc9e27cc512e1c00c0be4b38975fb6d5b"
|
|
372
|
+
},
|
|
373
|
+
{
|
|
374
|
+
"path": "bootstrap/platform/darwin/hellolynk-keychain-helper.bin",
|
|
375
|
+
"sha256": "f1a782c08229ebc0e8d1704b6390e91c5695e084b4df0a1a6dc043f6b23700ab"
|
|
376
|
+
},
|
|
361
377
|
{
|
|
362
378
|
"path": "release/server/codex-neutral-production.mjs",
|
|
363
|
-
"sha256": "
|
|
379
|
+
"sha256": "a307100c0b5835f5a6cba5151db367515b7f6cedd09d16af914f28e163df107a"
|
|
364
380
|
},
|
|
365
381
|
{
|
|
366
382
|
"path": "release/server/neutral-host-adapter.mjs",
|
|
Binary file
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
// Rebuild in this directory: xcrun swiftc -O -target arm64-apple-macos13.0 -module-name HelloLynkKeychain hellolynk-keychain-helper.swift -o hellolynk-keychain-helper.bin
|
|
2
|
+
import AppKit
|
|
3
|
+
import CryptoKit
|
|
4
|
+
import Foundation
|
|
5
|
+
import Security
|
|
6
|
+
|
|
7
|
+
func fail(_ code: String) -> Never {
|
|
8
|
+
FileHandle.standardError.write(Data((code + "\n").utf8))
|
|
9
|
+
exit(1)
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
func requireNoGrantACL(_ path: String) {
|
|
13
|
+
guard let acl = acl_get_file(path, ACL_TYPE_EXTENDED) else {
|
|
14
|
+
if errno == ENOENT { return } // A first write creates the file with mode 0600.
|
|
15
|
+
fail("macos_vault_acl_unavailable")
|
|
16
|
+
}
|
|
17
|
+
defer { acl_free(UnsafeMutableRawPointer(acl)) }
|
|
18
|
+
var entry: acl_entry_t?
|
|
19
|
+
var selector = ACL_FIRST_ENTRY
|
|
20
|
+
while acl_get_entry(acl, Int32(selector.rawValue), &entry) == 0 {
|
|
21
|
+
var tag = ACL_UNDEFINED_TAG
|
|
22
|
+
guard acl_get_tag_type(entry, &tag) == 0, tag == ACL_EXTENDED_DENY else {
|
|
23
|
+
fail("macos_vault_acl_invalid")
|
|
24
|
+
}
|
|
25
|
+
selector = ACL_NEXT_ENTRY
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// The caller verifies the release path's ancestors and package hashes. Walk
|
|
30
|
+
// with lstat so a symlink or hard link cannot redirect a permission change.
|
|
31
|
+
func secureRelease(_ path: String, verifyOnly: Bool) {
|
|
32
|
+
var pending = [path]
|
|
33
|
+
while let entry = pending.popLast() {
|
|
34
|
+
var st = stat()
|
|
35
|
+
guard lstat(entry, &st) == 0, st.st_uid == getuid() else { fail("macos_vault_release_invalid") }
|
|
36
|
+
let type = st.st_mode & S_IFMT
|
|
37
|
+
guard type == S_IFDIR || (type == S_IFREG && st.st_nlink == 1) else { fail("macos_vault_release_invalid") }
|
|
38
|
+
let mode: mode_t = type == S_IFDIR || entry.hasSuffix(".bin") ? 0o700 : 0o600
|
|
39
|
+
if (st.st_mode & 0o7777) != mode {
|
|
40
|
+
guard !verifyOnly, chmod(entry, mode) == 0 else { fail("macos_vault_release_permissions_invalid") }
|
|
41
|
+
}
|
|
42
|
+
requireNoGrantACL(entry)
|
|
43
|
+
if type == S_IFDIR {
|
|
44
|
+
guard let names = try? FileManager.default.contentsOfDirectory(atPath: entry) else { fail("macos_vault_release_invalid") }
|
|
45
|
+
pending.append(contentsOf: names.map { entry + "/" + $0 })
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
func masterKey(_ root: String, create: Bool) -> Data {
|
|
51
|
+
let account = SHA256.hash(data: Data(root.utf8)).map { String(format: "%02x", $0) }.joined()
|
|
52
|
+
let base: [String: Any] = [
|
|
53
|
+
kSecClass as String: kSecClassGenericPassword,
|
|
54
|
+
kSecAttrService as String: "com.hellolynk.codex.macos-vault.v1",
|
|
55
|
+
kSecAttrAccount as String: account
|
|
56
|
+
]
|
|
57
|
+
var lookup = base
|
|
58
|
+
lookup[kSecReturnData as String] = true
|
|
59
|
+
lookup[kSecMatchLimit as String] = kSecMatchLimitOne
|
|
60
|
+
var result: CFTypeRef?
|
|
61
|
+
let status = SecItemCopyMatching(lookup as CFDictionary, &result)
|
|
62
|
+
if status == errSecSuccess {
|
|
63
|
+
guard let key = result as? Data, key.count == 32 else { fail("macos_vault_key_invalid") }
|
|
64
|
+
return key
|
|
65
|
+
}
|
|
66
|
+
guard status == errSecItemNotFound else { fail("macos_keychain_access_\(abs(Int(status)))") }
|
|
67
|
+
guard create else { fail("macos_vault_key_missing") }
|
|
68
|
+
let key = SymmetricKey(size: .bits256).withUnsafeBytes { Data($0) }
|
|
69
|
+
var add = base
|
|
70
|
+
add[kSecValueData as String] = key
|
|
71
|
+
add[kSecAttrAccessible as String] = kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly
|
|
72
|
+
let inserted = SecItemAdd(add as CFDictionary, nil)
|
|
73
|
+
if inserted == errSecDuplicateItem { return masterKey(root, create: false) }
|
|
74
|
+
guard inserted == errSecSuccess else { fail("macos_keychain_access_\(abs(Int(inserted)))") }
|
|
75
|
+
return key
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
func nativeConfirmation() {
|
|
79
|
+
let raw = FileHandle.standardInput.readDataToEndOfFile()
|
|
80
|
+
guard raw.count <= 20480 * 6 + 8192,
|
|
81
|
+
var v = (try? JSONSerialization.jsonObject(with: raw)) as? [String: Any],
|
|
82
|
+
let body = v["displayText"] as? String, !body.isEmpty, body.utf8.count <= 20480,
|
|
83
|
+
let encoded = v["hmacKey"] as? String, encoded.count == 43,
|
|
84
|
+
let key = Data(base64Encoded: encoded.replacingOccurrences(of: "-", with: "+")
|
|
85
|
+
.replacingOccurrences(of: "_", with: "/") + "="), key.count == 32,
|
|
86
|
+
let recipient = v["recipientHelloLynkId"] as? String,
|
|
87
|
+
v["contract"] as? String == "hellolynk-native-human-confirmation-v1",
|
|
88
|
+
v["textDigest"] as? String == SHA256.hash(data: Data(body.utf8)).map({ String(format: "%02x", $0) }).joined()
|
|
89
|
+
else { fail("human_confirmation_request_invalid") }
|
|
90
|
+
|
|
91
|
+
var fields = ["contract", "actionId", "decision", "principalId", "delegateId", "hostInstallationId",
|
|
92
|
+
"adapterId", "adapterInstanceId", "hostGeneration", "recipientHelloLynkId"]
|
|
93
|
+
var label = "收件人:" + recipient
|
|
94
|
+
if v["recipientDelegateId"] != nil || v["recipientInstanceDisplayName"] != nil {
|
|
95
|
+
guard let name = v["recipientInstanceDisplayName"] as? String, !name.isEmpty, name.utf8.count <= 512,
|
|
96
|
+
name.range(of: #"[\x00-\x1F\x7F-\x9F\u2028\u2029\u202A-\u202E\u2066-\u2069]"#, options: .regularExpression) == nil,
|
|
97
|
+
v["recipientDelegateId"] is String else { fail("human_confirmation_request_invalid") }
|
|
98
|
+
fields += ["recipientDelegateId", "recipientInstanceDisplayName"]
|
|
99
|
+
label += "\n目标 AI:" + name
|
|
100
|
+
}
|
|
101
|
+
fields += ["clientMessageId", "textDigest", "createdAtMs", "expiresAtMs", "challenge"]
|
|
102
|
+
var expected = Set(fields)
|
|
103
|
+
expected.remove("decision")
|
|
104
|
+
expected.formUnion(["displayText", "hmacKey"])
|
|
105
|
+
guard expected == Set(v.keys) else { fail("human_confirmation_request_invalid") }
|
|
106
|
+
let app = NSApplication.shared
|
|
107
|
+
app.setActivationPolicy(.accessory)
|
|
108
|
+
app.activate(ignoringOtherApps: true)
|
|
109
|
+
let dialog = NSAlert()
|
|
110
|
+
dialog.messageText = "是否允许 HelloLynk 发送以下文本?"
|
|
111
|
+
dialog.informativeText = label
|
|
112
|
+
dialog.addButton(withTitle: "取消")
|
|
113
|
+
dialog.addButton(withTitle: "批准发送")
|
|
114
|
+
let scroll = NSScrollView(frame: NSRect(x: 0, y: 0, width: 500, height: 230))
|
|
115
|
+
scroll.hasVerticalScroller = true
|
|
116
|
+
let view = NSTextView(frame: scroll.contentView.bounds)
|
|
117
|
+
view.isEditable = false
|
|
118
|
+
view.isRichText = false
|
|
119
|
+
view.isVerticallyResizable = true
|
|
120
|
+
view.autoresizingMask = [.width]
|
|
121
|
+
view.textContainer?.widthTracksTextView = true
|
|
122
|
+
view.string = body
|
|
123
|
+
scroll.documentView = view
|
|
124
|
+
dialog.accessoryView = scroll
|
|
125
|
+
v["decision"] = dialog.runModal() == .alertSecondButtonReturn ? "approve" : "cancel"
|
|
126
|
+
let message = fields.map { field -> String in
|
|
127
|
+
if let text = v[field] as? String { return text }
|
|
128
|
+
if ["createdAtMs","expiresAtMs"].contains(field), let n = v[field] as? NSNumber { return n.stringValue }
|
|
129
|
+
fail("human_confirmation_request_invalid")
|
|
130
|
+
}.joined(separator: "\n")
|
|
131
|
+
let proof = HMAC<SHA256>.authenticationCode(for: Data(message.utf8), using: SymmetricKey(data: key))
|
|
132
|
+
v["proof"] = proof.map { String(format: "%02x", $0) }.joined()
|
|
133
|
+
for field in ["hmacKey", "challenge", "displayText"] { v.removeValue(forKey: field) }
|
|
134
|
+
guard let output = try? JSONSerialization.data(withJSONObject: v) else { fail("human_confirmation_response_invalid") }
|
|
135
|
+
FileHandle.standardOutput.write(output)
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
let args = Array(CommandLine.arguments.dropFirst())
|
|
139
|
+
if args == ["confirm"] { nativeConfirmation() }
|
|
140
|
+
else if args.count == 3, args[0] == "key", args[1].hasPrefix("/"), ["load","create"].contains(args[2]) {
|
|
141
|
+
requireNoGrantACL(args[1])
|
|
142
|
+
requireNoGrantACL(args[1] + "/protected-v1.sqlite")
|
|
143
|
+
let key = masterKey(args[1], create: args[2] == "create")
|
|
144
|
+
FileHandle(fileDescriptor: 3, closeOnDealloc: false).write(key)
|
|
145
|
+
} else if args.count == 2, ["verify-tree", "secure-tree"].contains(args[0]), args[1].hasPrefix("/") {
|
|
146
|
+
secureRelease(args[1], verifyOnly: args[0] == "verify-tree")
|
|
147
|
+
} else if args.count == 2, args[0] == "verify-acl", args[1].hasPrefix("/") {
|
|
148
|
+
requireNoGrantACL(args[1])
|
|
149
|
+
} else { fail("macos_vault_helper_arguments_invalid") }
|
|
@@ -30,11 +30,11 @@ const TRUSTED_BOOTSTRAP_FILES = Object.freeze(new Map([
|
|
|
30
30
|
["runtime/bootstrap/core/hellolynk-neutral-host-installation-store.mjs", "42ae1e7deb876a8308b7bfed6a7b891d1834117b62504fc5befe3f91a0b4c740"],
|
|
31
31
|
["runtime/bootstrap/core/hellolynk-host-auth.mjs", "db1ec56f4e60e6450fba50188eecf89748f39f5d7a75c2456c30a6053233555d"],
|
|
32
32
|
["runtime/bootstrap/core/hellolynk-client-profile.mjs", "8be37831c96610e3d0a64848ada86488970cc6079e49764b47d8a8b2ba852269"],
|
|
33
|
-
["runtime/bootstrap/core/hellolynk-platform-credential-store.mjs", "
|
|
34
|
-
["runtime/bootstrap/core/hellolynk-client-core.mjs", "
|
|
33
|
+
["runtime/bootstrap/core/hellolynk-platform-credential-store.mjs", "4b4b27cd86f4816e5e6ec4914816a335cdea5c4b4ffb2091775c061a9676a69c"],
|
|
34
|
+
["runtime/bootstrap/core/hellolynk-client-core.mjs", "dbab0f067c1a7f02c43b2696e1f5972288acd4f93a18684bf3cc120edff5cf94"],
|
|
35
35
|
["runtime/bootstrap/platform/windows/hellolynk-host-protected-store.ps1", "24357e8ce54090b4ec68e457f02c9e834a80f3273ef3c03a8c0fa7258797fb49"],
|
|
36
36
|
...(process.platform === "darwin" ? [
|
|
37
|
-
["runtime/bootstrap/platform/darwin/hellolynk-keychain-helper.bin", "
|
|
37
|
+
["runtime/bootstrap/platform/darwin/hellolynk-keychain-helper.bin", "f1a782c08229ebc0e8d1704b6390e91c5695e084b4df0a1a6dc043f6b23700ab"],
|
|
38
38
|
] : []),
|
|
39
39
|
]));
|
|
40
40
|
|