@hellolynk-ai/codex-plugin 0.2.102-macos.1 → 0.2.103

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (26) hide show
  1. package/README.md +17 -17
  2. package/bin/hellolynk-client-setup.mjs +7 -2
  3. package/hellolynk-client-pack-manifest.json +36 -56
  4. package/hellolynk-client-pack-pin.json +1 -1
  5. package/package.json +4 -8
  6. package/plugins/hellolynk-codex/.codex-plugin/plugin.json +1 -1
  7. package/plugins/hellolynk-codex/.mcp.json +19 -20
  8. package/plugins/hellolynk-codex/package.json +1 -1
  9. package/plugins/hellolynk-codex/runtime/bootstrap/core/hellolynk-darwin-host.mjs +45 -32
  10. package/plugins/hellolynk-codex/runtime/core/hellolynk-client-update-entry.mjs +3 -3
  11. package/plugins/hellolynk-codex/runtime/core/hellolynk-client-update-runtime.mjs +47 -34
  12. package/plugins/hellolynk-codex/runtime/core/hellolynk-client-update.mjs +46 -16
  13. package/plugins/hellolynk-codex/runtime/core/hellolynk-darwin-host.mjs +45 -32
  14. package/plugins/hellolynk-codex/runtime/core/hellolynk-neutral-host-process.mjs +1 -1
  15. package/plugins/hellolynk-codex/runtime/core/hellolynk-release-retention.mjs +34 -11
  16. package/plugins/hellolynk-codex/runtime/neutral-host-release-manifest.json +11 -27
  17. package/plugins/hellolynk-codex/runtime/neutral-host-release-pin.json +1 -1
  18. package/plugins/hellolynk-codex/runtime/platform/windows/credential-store.ps1 +46 -10
  19. package/plugins/hellolynk-codex/server/codex-neutral-production.mjs +2 -2
  20. package/plugins/hellolynk-codex/server/mcp-server.mjs +1193 -1193
  21. package/plugins/hellolynk-codex/server/native-conversation-directory.mjs +33 -33
  22. package/plugins/hellolynk-codex/server/workbench-runtime.mjs +42 -42
  23. package/plugins/hellolynk-codex/runtime/bootstrap/platform/darwin/hellolynk-keychain-helper.bin +0 -0
  24. package/plugins/hellolynk-codex/runtime/bootstrap/platform/darwin/hellolynk-keychain-helper.swift +0 -149
  25. package/plugins/hellolynk-codex/runtime/platform/darwin/hellolynk-keychain-helper.bin +0 -0
  26. package/plugins/hellolynk-codex/runtime/platform/darwin/hellolynk-keychain-helper.swift +0 -149
@@ -1,33 +1,33 @@
1
- import fs from "node:fs/promises";
2
- import os from "node:os";
3
- import path from "node:path";
4
-
5
- // The desktop's metadata index, read-only. Never read rollout bodies, previews,
6
- // first_user_message or model summaries. Native read_thread still decides
7
- // whether a selected conversation can actually receive a reminder.
8
- export async function readCodexConversationDirectory({ home = process.env.CODEX_HOME || path.join(os.homedir(), ".codex"),
9
- callerThreadId, projects = [], openDatabase } = {}) {
10
- const names = (await fs.readdir(home)).filter(name => /^state_\d+\.sqlite$/.test(name))
11
- .sort((a, b) => Number(b.match(/\d+/)[0]) - Number(a.match(/\d+/)[0]));
12
- if (!names.length) throw new Error("codex_native_directory_unavailable");
13
- const Database = openDatabase || ((await import("node:sqlite")).DatabaseSync);
14
- const db = new Database(path.join(home, names[0]), { readOnly: true });
15
- try {
16
- const columns = new Set(db.prepare("PRAGMA table_info(threads)").all().map(row => row.name));
17
- if (!["id", "title", "cwd", "source", "archived", "creator_account_id"].every(key => columns.has(key)))
18
- throw new Error("codex_native_directory_schema_unavailable");
19
- const owner = db.prepare("SELECT creator_account_id FROM threads WHERE id = ?").get(callerThreadId);
20
- if (!owner) throw new Error("codex_native_directory_owner_unavailable");
21
- const optional = ["name", "project_id"].filter(key => columns.has(key));
22
- const rows = db.prepare(`SELECT id, cwd, archived${optional.length ? ", " + optional.join(", ") : ""}
23
- FROM threads WHERE creator_account_id IS ? AND source IN ('vscode','cli','exec','appServer')`).all(owner.creator_account_id);
24
- const byProject = new Map(projects.map(project => [project.id || project.projectId, project.name || project.title]));
25
- return { coverage: "complete", exactTargetSupported: true, conversations: rows.map(row => ({
26
- // In the native index, title can be the complete first user message.
27
- // Only the explicit conversation name is metadata safe to export.
28
- targetConversationId: row.id, title: row.name || row.id,
29
- project: byProject.get(row.project_id) || path.win32.basename(row.cwd || "") || path.posix.basename(row.cwd || ""),
30
- state: row.archived ? "archived" : "notLoaded",
31
- })) };
32
- } finally { db.close(); }
33
- }
1
+ import fs from "node:fs/promises";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+
5
+ // The desktop's metadata index, read-only. Never read rollout bodies, previews,
6
+ // first_user_message or model summaries. Native read_thread still decides
7
+ // whether a selected conversation can actually receive a reminder.
8
+ export async function readCodexConversationDirectory({ home = process.env.CODEX_HOME || path.join(os.homedir(), ".codex"),
9
+ callerThreadId, projects = [], openDatabase } = {}) {
10
+ const names = (await fs.readdir(home)).filter(name => /^state_\d+\.sqlite$/.test(name))
11
+ .sort((a, b) => Number(b.match(/\d+/)[0]) - Number(a.match(/\d+/)[0]));
12
+ if (!names.length) throw new Error("codex_native_directory_unavailable");
13
+ const Database = openDatabase || ((await import("node:sqlite")).DatabaseSync);
14
+ const db = new Database(path.join(home, names[0]), { readOnly: true });
15
+ try {
16
+ const columns = new Set(db.prepare("PRAGMA table_info(threads)").all().map(row => row.name));
17
+ if (!["id", "title", "cwd", "source", "archived", "creator_account_id"].every(key => columns.has(key)))
18
+ throw new Error("codex_native_directory_schema_unavailable");
19
+ const owner = db.prepare("SELECT creator_account_id FROM threads WHERE id = ?").get(callerThreadId);
20
+ if (!owner) throw new Error("codex_native_directory_owner_unavailable");
21
+ const optional = ["name", "project_id"].filter(key => columns.has(key));
22
+ const rows = db.prepare(`SELECT id, cwd, archived${optional.length ? ", " + optional.join(", ") : ""}
23
+ FROM threads WHERE creator_account_id IS ? AND source IN ('vscode','cli','exec','appServer')`).all(owner.creator_account_id);
24
+ const byProject = new Map(projects.map(project => [project.id || project.projectId, project.name || project.title]));
25
+ return { coverage: "complete", exactTargetSupported: true, conversations: rows.map(row => ({
26
+ // In the native index, title can be the complete first user message.
27
+ // Only the explicit conversation name is metadata safe to export.
28
+ targetConversationId: row.id, title: row.name || row.id,
29
+ project: byProject.get(row.project_id) || path.win32.basename(row.cwd || "") || path.posix.basename(row.cwd || ""),
30
+ state: row.archived ? "archived" : "notLoaded",
31
+ })) };
32
+ } finally { db.close(); }
33
+ }
@@ -1,18 +1,18 @@
1
- import fs from "node:fs/promises";
2
- import { createHash } from "node:crypto";
3
- import { fileURLToPath } from "node:url";
4
-
5
- // Codex uses resource metadata before the app can report its intrinsic size.
6
- // Keep a usable frame when its initial container limit is echoed by the app.
7
- export const CODEX_WORKBENCH_RESOURCE_META = Object.freeze({
8
- ui: Object.freeze({ prefersBorder: true, domain: "hellolynk.local" }),
9
- "openai/widgetPrefersBorder": true,
10
- "openai/widgetHeightHint": 680,
11
- "openai/widgetMinFrameHeight": 680,
12
- });
13
-
14
- const CODEX_RESOURCE_URI_SEED = "ui://hellolynk/workbench-codex-pending.html";
15
- const OUTPUT_SHA256_RE = /^[a-f0-9]{64}$/;
1
+ import fs from "node:fs/promises";
2
+ import { createHash } from "node:crypto";
3
+ import { fileURLToPath } from "node:url";
4
+
5
+ // Codex uses resource metadata before the app can report its intrinsic size.
6
+ // Keep a usable frame when its initial container limit is echoed by the app.
7
+ export const CODEX_WORKBENCH_RESOURCE_META = Object.freeze({
8
+ ui: Object.freeze({ prefersBorder: true, domain: "hellolynk.local" }),
9
+ "openai/widgetPrefersBorder": true,
10
+ "openai/widgetHeightHint": 680,
11
+ "openai/widgetMinFrameHeight": 680,
12
+ });
13
+
14
+ const CODEX_RESOURCE_URI_SEED = "ui://hellolynk/workbench-codex-pending.html";
15
+ const OUTPUT_SHA256_RE = /^[a-f0-9]{64}$/;
16
16
  const SHARED_ROOTS = Object.freeze([
17
17
  new URL("../runtime/workbench/", import.meta.url),
18
18
  new URL("../../../clients/workbench/", import.meta.url),
@@ -38,14 +38,14 @@ export async function bootstrapCodexWorkbenchRuntime(options = {}) {
38
38
  || typeof shared.validateWorkbenchCommandResult !== "function") {
39
39
  throw codeError("workbench_codex_shared_bootstrap_invalid");
40
40
  }
41
- const explicitResourceUri = options.resourceUri == null ? null : options.resourceUri;
42
- const bootstrapped = await shared.bootstrapWorkbenchCandidate("codex", {
43
- resourceUri: explicitResourceUri || CODEX_RESOURCE_URI_SEED,
44
- });
45
- const resourceUri = explicitResourceUri || codexWorkbenchResourceUri(bootstrapped);
46
- const candidate = resourceUri === bootstrapped.resourceUri
47
- ? bootstrapped
48
- : Object.freeze({ ...bootstrapped, resourceUri });
41
+ const explicitResourceUri = options.resourceUri == null ? null : options.resourceUri;
42
+ const bootstrapped = await shared.bootstrapWorkbenchCandidate("codex", {
43
+ resourceUri: explicitResourceUri || CODEX_RESOURCE_URI_SEED,
44
+ });
45
+ const resourceUri = explicitResourceUri || codexWorkbenchResourceUri(bootstrapped);
46
+ const candidate = resourceUri === bootstrapped.resourceUri
47
+ ? bootstrapped
48
+ : Object.freeze({ ...bootstrapped, resourceUri });
49
49
  if (options.adapter === undefined) return candidate;
50
50
  if (typeof options.adapter?.readWorkbenchSnapshot !== "function") {
51
51
  throw codeError("workbench_codex_rpc_adapter_invalid");
@@ -58,9 +58,9 @@ export async function bootstrapCodexWorkbenchRuntime(options = {}) {
58
58
  const readOnlyCandidate = {
59
59
  resourceUri: candidate.resourceUri,
60
60
  html: candidate.html,
61
- readSnapshot: (input = {}) => options.adapter.readWorkbenchSnapshot(input),
62
- ...(typeof options.adapter.readWorkbenchChanges === "function"
63
- ? { readChanges: (input) => options.adapter.readWorkbenchChanges(input) } : {}),
61
+ readSnapshot: (input = {}) => options.adapter.readWorkbenchSnapshot(input),
62
+ ...(typeof options.adapter.readWorkbenchChanges === "function"
63
+ ? { readChanges: (input) => options.adapter.readWorkbenchChanges(input) } : {}),
64
64
  ...(typeof options.connectAccount === "function" ? { connectAccount: options.connectAccount } : {}),
65
65
  validateReadRequest: shared.validateWorkbenchReadRequest,
66
66
  validateSnapshot: shared.validateWorkbenchSnapshot,
@@ -82,24 +82,24 @@ export async function bootstrapCodexWorkbenchRuntime(options = {}) {
82
82
  validateCommand: shared.validateWorkbenchCommand,
83
83
  validateCommandResult: shared.validateWorkbenchCommandResult,
84
84
  });
85
- }
86
-
87
- function codexWorkbenchResourceUri(candidate) {
88
- const outputSha256 = String(candidate?.manifest?.outputSha256 || "");
89
- if (!OUTPUT_SHA256_RE.test(outputSha256)) throw codeError("workbench_codex_output_sha256_invalid");
90
- // Metadata changes must also invalidate Codex's cached resource.
91
- const resourceSha256 = createHash("sha256")
92
- .update(JSON.stringify({ outputSha256, meta: CODEX_WORKBENCH_RESOURCE_META }))
93
- .digest("hex");
94
- return `ui://hellolynk/workbench-codex-${resourceSha256}.html`;
95
- }
85
+ }
86
+
87
+ function codexWorkbenchResourceUri(candidate) {
88
+ const outputSha256 = String(candidate?.manifest?.outputSha256 || "");
89
+ if (!OUTPUT_SHA256_RE.test(outputSha256)) throw codeError("workbench_codex_output_sha256_invalid");
90
+ // Metadata changes must also invalidate Codex's cached resource.
91
+ const resourceSha256 = createHash("sha256")
92
+ .update(JSON.stringify({ outputSha256, meta: CODEX_WORKBENCH_RESOURCE_META }))
93
+ .digest("hex");
94
+ return `ui://hellolynk/workbench-codex-${resourceSha256}.html`;
95
+ }
96
96
 
97
97
  function supportsF3ActionGrantTransport(shared, candidate, adapter) {
98
- return shared.HELLOLYNK_WORKBENCH_SNAPSHOT_CONTRACT === "hellolynk-workbench-snapshot-v12"
99
- && shared.HELLOLYNK_WORKBENCH_SNAPSHOT_SCHEMA_VERSION === 12
100
- && shared.HELLOLYNK_WORKBENCH_COMMAND_CONTRACT === "hellolynk-workbench-command-v6"
101
- && shared.HELLOLYNK_WORKBENCH_RESULT_CONTRACT === "hellolynk-workbench-command-result-v6"
102
- && shared.HELLOLYNK_WORKBENCH_SCHEMA_VERSION === 6
98
+ return shared.HELLOLYNK_WORKBENCH_SNAPSHOT_CONTRACT === "hellolynk-workbench-snapshot-v12"
99
+ && shared.HELLOLYNK_WORKBENCH_SNAPSHOT_SCHEMA_VERSION === 12
100
+ && shared.HELLOLYNK_WORKBENCH_COMMAND_CONTRACT === "hellolynk-workbench-command-v6"
101
+ && shared.HELLOLYNK_WORKBENCH_RESULT_CONTRACT === "hellolynk-workbench-command-result-v6"
102
+ && shared.HELLOLYNK_WORKBENCH_SCHEMA_VERSION === 6
103
103
  && candidate?.manifest?.sourceContract === shared.HELLOLYNK_WORKBENCH_SNAPSHOT_CONTRACT
104
104
  && typeof candidate.html === "string"
105
105
  && candidate.html.includes("hellolynk_workbench_action_grant")
@@ -1,149 +0,0 @@
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,149 +0,0 @@
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") }