@hellolynk-ai/codex-plugin 0.2.105 → 0.2.106-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 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
- Windows 10/11、Node.js 22.16 或更高版本、PowerShell 7,以及打开过一次的 Codex。
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
- 用安装包,在官网或 [GitHub Release](https://github.com/haoyun18881-beep/HelloLynk/releases) 下载 Codex 安装包,完整解压,在包含 `package` 文件夹的目录打开 PowerShell,运行:
49
+ 推荐用 npm,在终端运行:
50
50
 
51
- ```powershell
52
- node .\package\bin\hellolynk-client-setup.mjs init
51
+ ```sh
52
+ npm install -g @hellolynk-ai/codex-plugin@macos
53
+ hellolynk-codex init
53
54
  ```
54
55
 
55
- 用 npm,在 PowerShell 运行:
56
+ 用安装包,在官网或 [GitHub Release](https://github.com/haoyun18881-beep/HelloLynk/releases) 下载 Codex for Mac 安装包,完整解压,在包含 `package` 文件夹的目录打开 终端,运行:
56
57
 
57
- ```powershell
58
- npm install -g @hellolynk-ai/codex-plugin
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 .\package\bin\hellolynk-client-setup.mjs` 换成 `hellolynk-codex`,在任意目录运行。
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
- ```powershell
77
- node .\package\bin\hellolynk-client-setup.mjs connect --hellolynk-id hellolynk:intl:your-name
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
- ```powershell
87
- node .\package\bin\hellolynk-client-setup.mjs setup
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 .\package\bin\hellolynk-client-setup.mjs uninstall`。账号、联系人和聊天记录留在这台电脑上,重新安装后不用重新连接。
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 .\package\bin\hellolynk-client-setup.mjs doctor` 检查安装,把结果和错误码发到 [haoyun18881@gmail.com](mailto:haoyun18881@gmail.com)。
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.105",
5
+ "version": "0.2.106-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": 1713,
36
- "sha256": "dc87461d38712659f1d25a3964cd9ab468ef99d859d038900ac1a277328d103e"
35
+ "bytes": 1776,
36
+ "sha256": "104aebabe771c8b88d237679f0997c6494ba5007cab2b55cac56fe88a1c68c44"
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": "dc33e34ece594590424d3ec1508409e07a02741db5d93d368824e46f12c095a2"
46
+ "sha256": "1c05e06618d2e44cd3f78f8de999db93b434366280f8b78a9aca4d08565607ff"
47
47
  },
48
48
  {
49
49
  "path": "plugins/hellolynk-codex/.mcp.json",
50
- "bytes": 373,
51
- "sha256": "c349aeb9545e8ed97f2eb5b7f4809880dae2c545b01ec049057d8ebd3a2221cc"
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": "be5fc524bab2f659f8f1e851bd1c59c6edf2a5fd4394c1a917a8d5ee9321190e"
61
+ "sha256": "a1167379affa28c595008e826ff854536d75b34d440a11963428d978230170f1"
62
62
  },
63
63
  {
64
64
  "path": "plugins/hellolynk-codex/runtime/bootstrap/core/hellolynk-client-account.mjs",
@@ -115,6 +115,16 @@
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,
@@ -322,8 +332,8 @@
322
332
  },
323
333
  {
324
334
  "path": "plugins/hellolynk-codex/runtime/core/hellolynk-neutral-host-assembly.mjs",
325
- "bytes": 46294,
326
- "sha256": "287ad6e077c8da05799b970ffa6607d8c7a0ab54a4837de51516e6af0ef6695b"
335
+ "bytes": 46525,
336
+ "sha256": "b63c3371dfc08d8b15a4fd5ce84c61b40d9e7011424ccb3ff11466adb4f862cc"
327
337
  },
328
338
  {
329
339
  "path": "plugins/hellolynk-codex/runtime/core/hellolynk-neutral-host-installation-store.mjs",
@@ -337,8 +347,8 @@
337
347
  },
338
348
  {
339
349
  "path": "plugins/hellolynk-codex/runtime/core/hellolynk-neutral-host-process.mjs",
340
- "bytes": 225161,
341
- "sha256": "ad6809633f49f7316acf6ce5b8f71a13d7ba20d3b62a2700664421ba671b5369"
350
+ "bytes": 229709,
351
+ "sha256": "34b985f75bc693954eb66bb788fc2814482bb8bf402f14b72ea2faf5f70cf4ea"
342
352
  },
343
353
  {
344
354
  "path": "plugins/hellolynk-codex/runtime/core/hellolynk-neutral-host-store-adapters.mjs",
@@ -397,13 +407,23 @@
397
407
  },
398
408
  {
399
409
  "path": "plugins/hellolynk-codex/runtime/neutral-host-release-manifest.json",
400
- "bytes": 14038,
401
- "sha256": "e5fcbb78060749b928487d97b59bccb4a2762ddf2f00896637d472818c933564"
410
+ "bytes": 14698,
411
+ "sha256": "768e7ecebed323b212f5bfa3afd9bfd2250d2e6e34fd622157ce50ed34726ddc"
402
412
  },
403
413
  {
404
414
  "path": "plugins/hellolynk-codex/runtime/neutral-host-release-pin.json",
405
415
  "bytes": 146,
406
- "sha256": "207213795005e40f36d7b238d9f6c689925dc6c1d1ed6fc17449cf7f80777636"
416
+ "sha256": "e6fef162c3ca23119ea2512758599ccfa17c7b65bfd5c861282df934037b5a09"
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": "43b899064fe0e53ee8a73c719fcb3320fc92375e25c66718bfbd11b3066f91d1"
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": 4536,
631
- "sha256": "6876323b3a86b6cce25b84f6c49232553ca4f8b9abd0c12f595a9b3b971f4e76"
650
+ "bytes": 4578,
651
+ "sha256": "95cd3887026e6a275dce26e50667c1422fad356b59409ce15d6f84ef2207ed92"
632
652
  },
633
653
  {
634
654
  "path": "tools/hellolynk-activate-neutral-host-clients.mjs",
@@ -1,4 +1,4 @@
1
1
  {
2
2
  "contract": "hellolynk-client-npm-pack-pin-v1",
3
- "manifestSha256": "346f4ca1d82c430ac3a49f0182f84836cb7e2c089d5b05b548bd2b3cb13b30ab"
3
+ "manifestSha256": "0fb3c4bf711423d7fc881069bba0472aa675e1aac3cddb89bd9d68b3db1b1bd8"
4
4
  }
package/package.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@hellolynk-ai/codex-plugin",
3
- "version": "0.2.105",
3
+ "version": "0.2.106-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
- "win32"
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,6 +1,6 @@
1
1
  {
2
2
  "name": "hellolynk-codex",
3
- "version": "0.2.105",
3
+ "version": "0.2.106",
4
4
  "description": "HelloLynk 通讯、本地未读提醒与安全文字收发的 Codex 插件。",
5
5
  "author": {
6
6
  "name": "HelloLynk local project"
@@ -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
- "env_vars": [
13
- "HELLOLYNK_NEUTRAL_STORE_ROOT",
14
- "CODEX_APP_TOOLS_PIPE_PATH",
15
- "CODEX_THREAD_ID"
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,6 +1,6 @@
1
1
  {
2
2
  "name": "@hellolynk-ai/codex-plugin",
3
- "version": "0.2.105",
3
+ "version": "0.2.106",
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "description": "Project-local HelloLynk Messaging, unread notification, and secure messaging plugin for Codex.",
@@ -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") }
@@ -334,6 +334,9 @@ export async function startHelloLynkNeutralHostAssembly(options = {}, dependenci
334
334
  authorizationVersion: profile.authorizationVersion,
335
335
  identityKeyId: e2ee.identityKeyId,
336
336
  runtimeHeaders: () => runtimeHeaders(sessionProvider),
337
+ // Drops only this instance's cached runtime session, and only for a stale
338
+ // session, so the Host can verify it once more with the device credential.
339
+ clearRuntimeSession: (error) => sessionProvider.clear(error),
337
340
  e2ee,
338
341
  contactE2ee,
339
342
  }));
@@ -1,6 +1,7 @@
1
1
  import { nativeConversationId, nativeConversationQuery, nativeDirectoryResult, queryLocalNativeConversations } from "./hellolynk-native-conversations.mjs";
2
2
  import { DIRECT_TEXT_E2EE_MAX_TTL_MS } from "./hellolynk-direct-text-e2ee.mjs";
3
3
  import { runModelInbox } from "./hellolynk-model-inbox.mjs";
4
+ import { shouldInvalidateDeviceRuntimeSession } from "./hellolynk-client-core.mjs";
4
5
  import crypto from "node:crypto";
5
6
  import fs from "node:fs/promises";
6
7
  import http from "node:http";
@@ -34,7 +35,7 @@ export const HELLOLYNK_NEUTRAL_HOST_PROCESS_CONTRACT = "hellolynk-neutral-host-p
34
35
  export const HELLOLYNK_NEUTRAL_HOST_DISCOVERY_CONTRACT = "hellolynk-neutral-host-discovery-v1";
35
36
  // Raised with every release batch, together with the six client versions. A Host reports it
36
37
  // in its health; the higher release stays and the lower one makes room.
37
- export const HELLOLYNK_HOST_RELEASE_ORDINAL = 17;
38
+ export const HELLOLYNK_HOST_RELEASE_ORDINAL = 18;
38
39
 
39
40
  const INSTALLATION_LOCK_CONTRACT = "hellolynk-neutral-host-installation-lock-v2";
40
41
  const INSTALLATION_LOCK_MAX_BYTES = 4 * 1024;
@@ -97,9 +98,16 @@ const PREKEY_REFILL_RETRY_MIN_MS = 1_000;
97
98
  const PREKEY_REFILL_RETRY_MAX_MS = 60_000;
98
99
  const RELAY_RETRY_MIN_MS = 1_000;
99
100
  const RELAY_RETRY_MAX_MS = 60_000;
100
- const RELAY_AUTHORITY_DENIAL_CODES = new Set([
101
+ // Revocation of this Host or its transport credential fences the whole Host.
102
+ const HOST_AUTHORITY_DENIAL_CODES = new Set([
101
103
  "host_transport_host_revoked", "host_transport_credential_revoked", "host_transport_credential_inactive",
104
+ ]);
105
+ // A refusal of one instance's own device credential or delegate stops only that
106
+ // instance. The Host and its other instances keep receiving and sending. The
107
+ // server reports these when issuing or using that instance's runtime session.
108
+ const ENDPOINT_AUTHORITY_DENIAL_CODES = new Set([
102
109
  "device_credential_invalid", "device_credential_revoked", "delegate_revoked", "delegate_inactive",
110
+ "device_credential_identity_inactive", "device_credential_inactive", "runtime_identity_inactive",
103
111
  ]);
104
112
  const MESSAGE_LOCAL_CAPACITY_CODES = new Set(["local_message_store_too_large"]);
105
113
  const CONTACT_REQUEST_LOCAL_CAPACITY_CODES = new Set([
@@ -247,6 +255,9 @@ export async function startHelloLynkNeutralHostProcess(options = {}) {
247
255
  let receiverState = null;
248
256
  const relayAbortController = new AbortController();
249
257
  const relayProvisionedEndpoints = new Set();
258
+ // Refused instances and their refusal code. Only a later successful registration
259
+ // (the next relay connection or session rotation) readmits one.
260
+ const rejectedEndpoints = new Map();
250
261
  const relayState = { status: "connecting", failureCount: 0, nextAttemptAtMs: 0, errorCode: null };
251
262
  const rejectedInboundTimes = new WeakSet();
252
263
  let lastRejectedInbound = null;
@@ -600,6 +611,20 @@ export async function startHelloLynkNeutralHostProcess(options = {}) {
600
611
  lastRejectedInbound = { errorCode: error.code };
601
612
  continue;
602
613
  }
614
+ // The frame's own instance was refused (for example at ACK). Stop only that
615
+ // instance and skip the frame; the relay expires or redelivers it.
616
+ const refused = ENDPOINT_AUTHORITY_DENIAL_CODES.has(error?.code)
617
+ && endpoints.get(String(item.data?.recipientDelegateId || ""));
618
+ if (refused) {
619
+ rejectedEndpoints.set(refused.delegateId, error.code);
620
+ relayState.errorCode = error.code;
621
+ lastRejectedInbound = { errorCode: error.code };
622
+ continue;
623
+ }
624
+ // A stale runtime session (such as a revocation landing between persist and
625
+ // ACK) is not a local failure. Reconnect: registration verifies that instance
626
+ // again and isolates it if refused. The durable record stays; the relay redelivers.
627
+ if (shouldInvalidateDeviceRuntimeSession(error)) throw error;
603
628
  if (!isInboundLocalCapacityError(error)) {
604
629
  inboundFailed = true;
605
630
  throw error;
@@ -612,7 +637,7 @@ export async function startHelloLynkNeutralHostProcess(options = {}) {
612
637
  } catch (error) {
613
638
  if (lifecycle !== "running") return;
614
639
  // Preserve fail-closed persistence/ACK and other malformed-inbound errors.
615
- if (inboundFailed || RELAY_AUTHORITY_DENIAL_CODES.has(error?.code)) throw error;
640
+ if (inboundFailed || HOST_AUTHORITY_DENIAL_CODES.has(error?.code)) throw error;
616
641
  await stream?.close?.().catch(() => {});
617
642
  stream = null;
618
643
  scheduleRelayRetry(error);
@@ -638,31 +663,63 @@ export async function startHelloLynkNeutralHostProcess(options = {}) {
638
663
  const task = (async () => {
639
664
  let connected = null;
640
665
  try {
641
- const registrations = await Promise.allSettled([...endpoints.values()].map(async (endpoint) => {
642
- if (endpoint.e2ee?.preKeyInventoryStatus?.status === "deferred"
643
- && !relayProvisionedEndpoints.has(endpoint.delegateId)) {
644
- try {
645
- await endpoint.e2ee.ensurePreKeyInventory({ signal });
646
- } catch (error) {
647
- if (error?.code !== "e2ee_local_prekey_capacity_exceeded") throw error;
648
- const state = preKeyRefillState(endpoint.delegateId);
649
- state.failureCount = 1;
650
- state.nextAttemptAtMs = Math.max(nowMs(clock) + PREKEY_REFILL_RETRY_MIN_MS, retryAtTimestamp(error.retryAt) || 0);
651
- state.errorCode = error.code;
666
+ const attempted = [...endpoints.values()];
667
+ const registrations = await Promise.allSettled(attempted.map(async (endpoint) => {
668
+ const register = async () => {
669
+ if (endpoint.e2ee?.preKeyInventoryStatus?.status === "deferred"
670
+ && !relayProvisionedEndpoints.has(endpoint.delegateId)) {
671
+ try {
672
+ await endpoint.e2ee.ensurePreKeyInventory({ signal });
673
+ } catch (error) {
674
+ if (error?.code !== "e2ee_local_prekey_capacity_exceeded") throw error;
675
+ const state = preKeyRefillState(endpoint.delegateId);
676
+ state.failureCount = 1;
677
+ state.nextAttemptAtMs = Math.max(nowMs(clock) + PREKEY_REFILL_RETRY_MIN_MS, retryAtTimestamp(error.retryAt) || 0);
678
+ state.errorCode = error.code;
679
+ }
680
+ relayProvisionedEndpoints.add(endpoint.delegateId);
652
681
  }
653
- relayProvisionedEndpoints.add(endpoint.delegateId);
682
+ if (signal.aborted || lifecycle !== "running") return;
683
+ await options.transport.registerEndpoint({
684
+ delegateId: endpoint.delegateId, identityKeyId: endpoint.identityKeyId,
685
+ runtimeHeaders: endpoint.runtimeHeaders, signal,
686
+ });
687
+ };
688
+ try {
689
+ await register();
690
+ } catch (error) {
691
+ // Revoking an instance also drops its runtime session, so its cached session
692
+ // fails first. Clear only that stale cache and verify once more: a device
693
+ // refusal then isolates the instance; any other failure keeps the usual path.
694
+ const cleared = !shouldInvalidateDeviceRuntimeSession(error) || signal.aborted || lifecycle !== "running"
695
+ || typeof endpoint.clearRuntimeSession !== "function" ? false
696
+ : await Promise.resolve().then(() => endpoint.clearRuntimeSession(error)).catch(() => false);
697
+ if (cleared !== true) throw error;
698
+ await register();
654
699
  }
655
- if (signal.aborted || lifecycle !== "running") return;
656
- await options.transport.registerEndpoint({
657
- delegateId: endpoint.delegateId, identityKeyId: endpoint.identityKeyId,
658
- runtimeHeaders: endpoint.runtimeHeaders, signal,
659
- });
660
700
  }));
661
701
  if (signal.aborted || lifecycle !== "running") return;
662
- const failed = registrations.find((result) => result.status === "rejected"
663
- && RELAY_AUTHORITY_DENIAL_CODES.has(result.reason?.code))
664
- || registrations.find((result) => result.status === "rejected");
702
+ const hostDenial = registrations.find((result) => result.status === "rejected"
703
+ && HOST_AUTHORITY_DENIAL_CODES.has(result.reason?.code));
704
+ if (hostDenial) throw hostDenial.reason;
705
+ // A refused instance stays out of the relay while the others connect. Each
706
+ // later connection, including session rotation, registers it again.
707
+ registrations.forEach((result, index) => {
708
+ if (result.status === "fulfilled") rejectedEndpoints.delete(attempted[index].delegateId);
709
+ else if (ENDPOINT_AUTHORITY_DENIAL_CODES.has(result.reason?.code)) {
710
+ rejectedEndpoints.set(attempted[index].delegateId, result.reason.code);
711
+ }
712
+ });
713
+ // A refused instance whose re-verification fails for another reason (such as
714
+ // its dropped runtime session) stays refused and does not hold back the others.
715
+ const failed = registrations.find((result, index) => result.status === "rejected"
716
+ && !rejectedEndpoints.has(attempted[index].delegateId));
665
717
  if (failed) throw failed.reason;
718
+ // With every instance refused there is nothing to receive: keep the Host and
719
+ // retry with the usual backoff. The session manager bounds cloud rechecks.
720
+ if (rejectedEndpoints.size === endpoints.size) {
721
+ throw registrations.find((result) => result.status === "rejected").reason;
722
+ }
666
723
  connected = await openRelayStream(signal);
667
724
  if (!connected?.events || typeof connected.events[Symbol.asyncIterator] !== "function") {
668
725
  throw failure("neutral_host_transport_stream_invalid");
@@ -672,14 +729,15 @@ export async function startHelloLynkNeutralHostProcess(options = {}) {
672
729
  return;
673
730
  }
674
731
  stream = connected;
675
- Object.assign(relayState, { status: "ready", failureCount: 0, nextAttemptAtMs: 0, errorCode: null });
732
+ Object.assign(relayState, { status: "ready", failureCount: 0, nextAttemptAtMs: 0,
733
+ errorCode: rejectedEndpoints.values().next().value ?? null });
676
734
  startReceiveTask();
677
735
  } catch (error) {
678
736
  await connected?.close?.().catch(() => {});
679
737
  if (signal.aborted || lifecycle !== "running") return;
680
738
  // An explicit revocation of this Host still fences local access. A
681
739
  // timeout/expired cloud session alone is not a local revocation.
682
- if (RELAY_AUTHORITY_DENIAL_CODES.has(error?.code)) {
740
+ if (HOST_AUTHORITY_DENIAL_CODES.has(error?.code)) {
683
741
  // Failure is exposed as host.status/failureCode to the release entry.
684
742
  void beginDrain(error.code).catch(() => {});
685
743
  return;
@@ -733,6 +791,14 @@ export async function startHelloLynkNeutralHostProcess(options = {}) {
733
791
  }
734
792
  const endpoint = endpoints.get(safeId(event.recipientDelegateId, "neutral_host_inbound_delegate_invalid"));
735
793
  if (!endpoint) throw failure("neutral_host_inbound_endpoint_unknown");
794
+ if (rejectedEndpoints.has(endpoint.delegateId)) {
795
+ // A refused instance receives nothing, also not from its Host's siblings.
796
+ // Relay frames are skipped unacknowledged; a local sender sees the failure.
797
+ const refused = failure(item.localDelivery === true
798
+ ? "neutral_host_same_host_recipient_unavailable" : rejectedEndpoints.get(endpoint.delegateId));
799
+ rejectedInboundTimes.add(refused);
800
+ throw refused;
801
+ }
736
802
  const isContactRequest = item.event === "contact_request";
737
803
  if (!isContactRequest && item.event !== "message") throw failure("neutral_host_inbound_kind_invalid");
738
804
  const decryptor = isContactRequest ? endpoint.contactE2ee : endpoint.e2ee;
@@ -1015,7 +1081,14 @@ export async function startHelloLynkNeutralHostProcess(options = {}) {
1015
1081
  return inboundResumeTask;
1016
1082
  }
1017
1083
 
1084
+ // A refused instance does not send, including over local routes that skip the relay.
1085
+ function assertEndpointAccepted(delegateId) {
1086
+ const code = rejectedEndpoints.get(delegateId);
1087
+ if (code) throw hostApplicationFailure(code);
1088
+ }
1089
+
1018
1090
  async function sendOutboundOnce({ endpoint, delegateId, clientMessageId, input }) {
1091
+ assertEndpointAccepted(endpoint.delegateId);
1019
1092
  try {
1020
1093
  const result = await endpoint.outbound.send(input);
1021
1094
  if (result?.message?.lastErrorCode === "reply_target_deleted" || result?.relay?.status === "reply_target_deleted") {
@@ -1529,6 +1602,7 @@ export async function startHelloLynkNeutralHostProcess(options = {}) {
1529
1602
  async function queryPeerNativeDirectory(delegateId, authorization, request) {
1530
1603
  const sender = endpoints.get(delegateId);
1531
1604
  if (!sender?.outbound?.sendDirectory) throw hostApplicationFailure("native_directory_unavailable");
1605
+ assertEndpointAccepted(delegateId);
1532
1606
  const friend = request.kind === "friend_target_request";
1533
1607
  const unavailable = friend ? null : nativeDirectoryResult({ conversations: [], coverage: "unavailable",
1534
1608
  available: false, exactTargetSupported: false, reason: "native_directory_unavailable" });
@@ -2274,6 +2348,7 @@ export async function startHelloLynkNeutralHostProcess(options = {}) {
2274
2348
  || current.senderDelegateId !== context.delegateId) {
2275
2349
  throw hostApplicationFailure("host_app_record_not_found");
2276
2350
  }
2351
+ assertEndpointAccepted(context.delegateId);
2277
2352
  if (!MANUAL_RETRYABLE_STATUSES.has(current.status) || current.lastErrorCode === "reply_target_deleted") {
2278
2353
  throw hostApplicationFailure("host_app_state_conflict");
2279
2354
  }
@@ -3120,7 +3195,8 @@ export async function startHelloLynkNeutralHostProcess(options = {}) {
3120
3195
  return {
3121
3196
  status: inboundPaused
3122
3197
  ? "paused"
3123
- : preKeyInventory.status === "ready" && relayState.status === "ready" && !lastRejectedInbound ? "ready" : "degraded",
3198
+ : preKeyInventory.status === "ready" && relayState.status === "ready" && !lastRejectedInbound
3199
+ && rejectedEndpoints.size === 0 ? "ready" : "degraded",
3124
3200
  ...(lastRejectedInbound ? { lastRejectedInbound } : {}),
3125
3201
  preKeyInventory,
3126
3202
  relay: {
@@ -4,7 +4,7 @@
4
4
  "files": [
5
5
  {
6
6
  "path": "release/package.json",
7
- "sha256": "be5fc524bab2f659f8f1e851bd1c59c6edf2a5fd4394c1a917a8d5ee9321190e"
7
+ "sha256": "a1167379affa28c595008e826ff854536d75b34d440a11963428d978230170f1"
8
8
  },
9
9
  {
10
10
  "path": "neutral-host-bootstrap.mjs",
@@ -184,7 +184,7 @@
184
184
  },
185
185
  {
186
186
  "path": "core/hellolynk-neutral-host-assembly.mjs",
187
- "sha256": "287ad6e077c8da05799b970ffa6607d8c7a0ab54a4837de51516e6af0ef6695b"
187
+ "sha256": "b63c3371dfc08d8b15a4fd5ce84c61b40d9e7011424ccb3ff11466adb4f862cc"
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": "ad6809633f49f7316acf6ce5b8f71a13d7ba20d3b62a2700664421ba671b5369"
199
+ "sha256": "34b985f75bc693954eb66bb788fc2814482bb8bf402f14b72ea2faf5f70cf4ea"
200
200
  },
201
201
  {
202
202
  "path": "core/hellolynk-neutral-host-store-adapters.mjs",
@@ -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": "43b899064fe0e53ee8a73c719fcb3320fc92375e25c66718bfbd11b3066f91d1"
379
+ "sha256": "a307100c0b5835f5a6cba5151db367515b7f6cedd09d16af914f28e163df107a"
364
380
  },
365
381
  {
366
382
  "path": "release/server/neutral-host-adapter.mjs",
@@ -1,4 +1,4 @@
1
1
  {
2
2
  "contract": "hellolynk-neutral-host-release-pin-v1",
3
- "manifestSha256": "e5fcbb78060749b928487d97b59bccb4a2762ddf2f00896637d472818c933564"
3
+ "manifestSha256": "768e7ecebed323b212f5bfa3afd9bfd2250d2e6e34fd622157ce50ed34726ddc"
4
4
  }
@@ -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") }
@@ -34,7 +34,7 @@ const TRUSTED_BOOTSTRAP_FILES = Object.freeze(new Map([
34
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", "0000000000000000000000000000000000000000000000000000000000000000"],
37
+ ["runtime/bootstrap/platform/darwin/hellolynk-keychain-helper.bin", "f1a782c08229ebc0e8d1704b6390e91c5695e084b4df0a1a6dc043f6b23700ab"],
38
38
  ] : []),
39
39
  ]));
40
40