@hellolynk-ai/codex-plugin 0.2.102 → 0.2.103-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.
Files changed (42) hide show
  1. package/README.md +17 -17
  2. package/bin/hellolynk-client-setup.mjs +70 -10
  3. package/hellolynk-client-pack-manifest.json +101 -61
  4. package/hellolynk-client-pack-pin.json +1 -1
  5. package/package.json +8 -4
  6. package/plugins/hellolynk-codex/.codex-plugin/plugin.json +1 -1
  7. package/plugins/hellolynk-codex/.mcp.json +20 -19
  8. package/plugins/hellolynk-codex/package.json +1 -1
  9. package/plugins/hellolynk-codex/runtime/bootstrap/core/hellolynk-client-account.mjs +1 -1
  10. package/plugins/hellolynk-codex/runtime/bootstrap/core/hellolynk-darwin-host.mjs +269 -0
  11. package/plugins/hellolynk-codex/runtime/bootstrap/core/hellolynk-darwin-vault.mjs +153 -0
  12. package/plugins/hellolynk-codex/runtime/bootstrap/core/hellolynk-host-protected-store.mjs +20 -8
  13. package/plugins/hellolynk-codex/runtime/bootstrap/core/hellolynk-neutral-host-installation-store.mjs +1 -1
  14. package/plugins/hellolynk-codex/runtime/bootstrap/core/hellolynk-platform-credential-store.mjs +68 -14
  15. package/plugins/hellolynk-codex/runtime/bootstrap/platform/darwin/hellolynk-keychain-helper.bin +0 -0
  16. package/plugins/hellolynk-codex/runtime/bootstrap/platform/darwin/hellolynk-keychain-helper.swift +149 -0
  17. package/plugins/hellolynk-codex/runtime/connect-activate.mjs +5 -1
  18. package/plugins/hellolynk-codex/runtime/core/hellolynk-client-account.mjs +1 -1
  19. package/plugins/hellolynk-codex/runtime/core/hellolynk-client-update-entry.mjs +7 -3
  20. package/plugins/hellolynk-codex/runtime/core/hellolynk-client-update-runtime.mjs +62 -33
  21. package/plugins/hellolynk-codex/runtime/core/hellolynk-client-update.mjs +62 -18
  22. package/plugins/hellolynk-codex/runtime/core/hellolynk-darwin-host.mjs +269 -0
  23. package/plugins/hellolynk-codex/runtime/core/hellolynk-darwin-vault.mjs +153 -0
  24. package/plugins/hellolynk-codex/runtime/core/hellolynk-host-protected-store.mjs +20 -8
  25. package/plugins/hellolynk-codex/runtime/core/hellolynk-human-confirmation-ledger.mjs +53 -6
  26. package/plugins/hellolynk-codex/runtime/core/hellolynk-local-data-protection.mjs +22 -7
  27. package/plugins/hellolynk-codex/runtime/core/hellolynk-native-human-confirmation-broker.mjs +7 -5
  28. package/plugins/hellolynk-codex/runtime/core/hellolynk-neutral-host-installation-store.mjs +1 -1
  29. package/plugins/hellolynk-codex/runtime/core/hellolynk-neutral-host-launcher.mjs +9 -3
  30. package/plugins/hellolynk-codex/runtime/core/hellolynk-neutral-host-process.mjs +1 -1
  31. package/plugins/hellolynk-codex/runtime/core/hellolynk-platform-credential-store.mjs +68 -14
  32. package/plugins/hellolynk-codex/runtime/core/hellolynk-release-retention.mjs +101 -2
  33. package/plugins/hellolynk-codex/runtime/core/hellolynk-workbench-connection.mjs +5 -3
  34. package/plugins/hellolynk-codex/runtime/neutral-host-release-entry.mjs +1 -1
  35. package/plugins/hellolynk-codex/runtime/neutral-host-release-loader.mjs +1 -1
  36. package/plugins/hellolynk-codex/runtime/neutral-host-release-manifest.json +56 -24
  37. package/plugins/hellolynk-codex/runtime/neutral-host-release-pin.json +1 -1
  38. package/plugins/hellolynk-codex/runtime/platform/darwin/hellolynk-keychain-helper.bin +0 -0
  39. package/plugins/hellolynk-codex/runtime/platform/darwin/hellolynk-keychain-helper.swift +149 -0
  40. package/plugins/hellolynk-codex/runtime/platform/windows/credential-store.ps1 +46 -10
  41. package/plugins/hellolynk-codex/server/codex-neutral-production.mjs +14 -8
  42. package/tools/hellolynk-activate-neutral-host-clients.mjs +5 -1
@@ -1,3 +1,5 @@
1
+ import { invokeDarwinHost } from "./hellolynk-darwin-host.mjs";
2
+ import { checkDarwinPath, DARWIN_HELPER, invokeDarwinHelper } from "./hellolynk-darwin-vault.mjs";
1
3
  import { randomBytes } from "node:crypto";
2
4
  import { lstat, mkdir } from "node:fs/promises";
3
5
  import { createConnection } from "node:net";
@@ -142,7 +144,7 @@ const READ_ONLY_COMMANDS = new Set([
142
144
  ]);
143
145
 
144
146
  export function defaultClaudeCodeStoreRoot(env = process.env) {
145
- return defaultNeutralHostStoreRoot("win32", env);
147
+ return defaultNeutralHostStoreRoot(process.platform, env);
146
148
  }
147
149
 
148
150
  function assertClaudeCodeStoreRoot(root) {
@@ -154,9 +156,9 @@ function assertClaudeCodeStoreRoot(root) {
154
156
 
155
157
  export function createHostProtectedStore(options = {}) {
156
158
  const platform = options.platform || process.platform;
157
- if (platform !== "win32") throw codeError("host_protected_store_platform_unsupported");
159
+ if (!["darwin", "win32"].includes(platform)) throw codeError("host_protected_store_platform_unsupported");
158
160
  const root = assertRoot(options.root || defaultNeutralHostStoreRoot(platform, options.env));
159
- const helperPath = resolve(options.helperPath || SCRIPT_PATH);
161
+ const helperPath = resolve(options.helperPath || (platform === "darwin" ? DARWIN_HELPER : SCRIPT_PATH));
160
162
  const clock = normalizeClock(options.clock);
161
163
  const random = typeof options.randomBytes === "function" ? options.randomBytes : randomBytes;
162
164
  const readOnly = options.readOnly === true;
@@ -166,12 +168,12 @@ export function createHostProtectedStore(options = {}) {
166
168
  verifyPath: ({ path, directory }) => verifyHostProtectedStorePath({
167
169
  root,
168
170
  path,
169
- directory,
171
+ directory, platform,
170
172
  helperPath,
171
173
  signal: options.signal,
172
174
  }),
173
175
  } : { root });
174
- const nativeInvoke = (command, invokeOptions) => invokeWindowsStore({
176
+ const nativeInvoke = (command, invokeOptions) => (platform === "darwin" ? invokeDarwinHost : invokeWindowsStore)({
175
177
  command, root, helperPath, verifyOnly: readOnly, ...(readOnly ? { signal: options.signal } : {}), ...invokeOptions,
176
178
  });
177
179
  const baseInvoke = options.invoke || ((command, invokeOptions) => serializeRootInvoke(root, () => nativeInvoke(command, invokeOptions)));
@@ -180,7 +182,7 @@ export function createHostProtectedStore(options = {}) {
180
182
  // Each peer lookup creates a read-only store. Reuse its successful DPAPI
181
183
  // result across those instances, while checking live paths and the file
182
184
  // on every use. Registration/account checks remain in the public methods.
183
- if (readOnly && !options.invoke && NEUTRAL_BINDING_READ.test(command)) {
185
+ if (platform === "win32" && readOnly && !options.invoke && NEUTRAL_BINDING_READ.test(command)) {
184
186
  return readNeutralBinding(command, invokeOptions);
185
187
  }
186
188
  try {
@@ -271,6 +273,7 @@ export function createHostProtectedStore(options = {}) {
271
273
 
272
274
  async function resolveCapability(capabilityId) {
273
275
  const id = assertCapabilityId(capabilityId);
276
+ if (platform === "darwin") return currentCapability(await load(id));
274
277
  for (let attempt = 0; attempt < 2; attempt += 1) {
275
278
  const fingerprint = await readCapabilityStoreFingerprint();
276
279
  if (fingerprint !== capabilityCacheFingerprint) {
@@ -1347,13 +1350,22 @@ async function protectedPathFingerprint(root, target, helperPath, directory, sig
1347
1350
  }
1348
1351
 
1349
1352
  export async function verifyHostProtectedStorePath(options = {}) {
1350
- const root = assertRoot(options.root || defaultNeutralHostStoreRoot("win32", options.env));
1353
+ const platform = options.platform || process.platform;
1354
+ const root = assertRoot(options.root || defaultNeutralHostStoreRoot(platform, options.env));
1351
1355
  const target = resolve(String(options.path || ""));
1352
1356
  const relativeTarget = relative(root, target);
1353
1357
  if (!isAbsolute(String(options.path || ""))
1354
- || (relativeTarget !== "" && (isAbsolute(relativeTarget) || relativeTarget === ".." || relativeTarget.startsWith(`..\\`)))) {
1358
+ || (relativeTarget !== "" && (isAbsolute(relativeTarget) || relativeTarget === ".." || relativeTarget.startsWith("../") || relativeTarget.startsWith(`..\\`)))) {
1355
1359
  throw codeError("host_protected_store_path_invalid");
1356
1360
  }
1361
+ if (platform === "darwin") {
1362
+ checkDarwinPath(root, { directory: true, privateMode: true });
1363
+ checkDarwinPath(target, { directory: options.directory === true, privateMode: true });
1364
+ const helper = options.helperPath || DARWIN_HELPER;
1365
+ await invokeDarwinHelper(helper, ["verify-acl", root], options.signal);
1366
+ if (target !== root) await invokeDarwinHelper(helper, ["verify-acl", target], options.signal);
1367
+ return Object.freeze({ verified: true });
1368
+ }
1357
1369
  const helperPath = resolve(options.helperPath || SCRIPT_PATH);
1358
1370
  const directory = options.directory === true;
1359
1371
  const key = JSON.stringify([root, target, helperPath, directory]);
@@ -1,3 +1,4 @@
1
+ import { withDarwinVault } from "./hellolynk-darwin-vault.mjs";
1
2
  import { randomBytes } from "node:crypto";
2
3
  import { lstat } from "node:fs/promises";
3
4
  import { createConnection } from "node:net";
@@ -23,14 +24,15 @@ const TERMINAL_STATES = new Set([
23
24
  ]);
24
25
 
25
26
  export function createWindowsHumanConfirmationLedger(options = {}) {
26
- if ((options.platform || process.platform) !== "win32") {
27
+ const platform = options.platform || process.platform;
28
+ if (!["darwin", "win32"].includes(platform)) {
27
29
  throw error("human_confirmation_ledger_platform_unsupported");
28
30
  }
29
- const root = assertRoot(options.root);
31
+ const root = assertRoot(options.root, platform);
30
32
  const helperPath = resolve(options.helperPath || HELPER_PATH);
31
- const invoke = options.invoke || ((command, value) => invokeHelper({
32
- command, value, root, helperPath,
33
- }));
33
+ const invoke = options.invoke || (platform === "darwin"
34
+ ? (command, value) => darwinLedger(root, options.helperPath, command, value)
35
+ : (command, value) => invokeHelper({ command, value, root, helperPath }));
34
36
 
35
37
  async function admit(value) {
36
38
  const input = normalizeAdmission(value);
@@ -79,6 +81,45 @@ export function createWindowsHumanConfirmationLedger(options = {}) {
79
81
  });
80
82
  }
81
83
 
84
+ function darwinLedger(root, helperPath, command, value) {
85
+ return withDarwinVault({ root, helperPath, readOnly: command === "Lookup", allowMissing: true }, data => {
86
+ const now = Date.now();
87
+ const records = (data.records || []).filter(r => r.admittedAtMs >= now - 86_400_000 || (r.pending && r.expiresAtMs > now));
88
+ const current = records.find(r => r.actionId === value.actionId);
89
+ if (command === "Lookup") {
90
+ if (!current || current.partitionDigest !== value.partitionDigest) return {record:null};
91
+ const { actionId, state, code, createdAtMs, expiresAtMs } = current;
92
+ return { record: { actionId, state, code, createdAtMs, expiresAtMs } };
93
+ }
94
+ if (current && (current.partitionDigest !== value.partitionDigest || current.createdAtMs !== value.createdAtMs ||
95
+ current.expiresAtMs !== value.expiresAtMs)) throw error("human_confirmation_ledger_action_conflict");
96
+ if (command === "Admit") {
97
+ if (current) return { accepted:true };
98
+ const partition = records.filter(r => r.partitionDigest === value.partitionDigest);
99
+ if (partition.some(r => r.pending && r.expiresAtMs > value.createdAtMs)) return { accepted:false, reason:"partition_pending" };
100
+ const recent = partition.filter(r => r.admittedAtMs > now - value.rateWindowMs);
101
+ if (recent.length >= value.rateLimit) return { accepted:false, reason:"rate_limited",
102
+ retryAfterMs:Math.max(0, Math.min(...recent.map(r => r.admittedAtMs)) + value.rateWindowMs - now) };
103
+ if (records.length >= 4096) throw error("human_confirmation_ledger_capacity_exceeded");
104
+ const { actionId, partitionDigest, createdAtMs, expiresAtMs } = value;
105
+ records.push({ actionId, partitionDigest, createdAtMs, expiresAtMs, admittedAtMs:now, pending:true,
106
+ state:"invalidated", code:"human_confirmation_restart_invalidated", recordedAtMs:createdAtMs });
107
+ } else {
108
+ if (!current) throw error("human_confirmation_ledger_action_missing");
109
+ const { state, code, recordedAtMs } = value;
110
+ if (current.state === state && current.code === code && current.recordedAtMs === recordedAtMs) return { recorded:true };
111
+ const fromPending = current.pending && current.state === "invalidated" && state !== "approved";
112
+ const fromConsuming = current.state === "consuming" && ["approved","failed"].includes(state);
113
+ if (!(fromPending || fromConsuming) || recordedAtMs < current.recordedAtMs) throw error("human_confirmation_ledger_state_regression");
114
+ if (state === "consuming" && recordedAtMs >= current.expiresAtMs) throw error("human_confirmation_timed_out");
115
+ Object.assign(current, {state, code, recordedAtMs, pending:false});
116
+ }
117
+ if (Buffer.byteLength(JSON.stringify(records)) > 1048576) throw error("human_confirmation_ledger_too_large");
118
+ data.records = records;
119
+ return command === "Admit" ? { accepted:true } : { recorded:true };
120
+ });
121
+ }
122
+
82
123
  function normalizeAdmission(value) {
83
124
  assertExact(value, [
84
125
  "actionId", "contract", "createdAtMs", "expiresAtMs", "partitionHash",
@@ -237,8 +278,14 @@ function publicCode(value) {
237
278
  : "human_confirmation_ledger_operation_failed";
238
279
  }
239
280
 
240
- function assertRoot(value) {
281
+ function assertRoot(value, platform) {
241
282
  const raw = String(value || "");
283
+ if (platform === "darwin") {
284
+ if (!isAbsolute(raw) || raw !== resolve(raw) || raw === "/" || raw.includes("\0")) {
285
+ throw error("human_confirmation_ledger_root_invalid");
286
+ }
287
+ return raw;
288
+ }
242
289
  if (!/^[A-Za-z]:[\\/]/.test(raw) || raw.startsWith("\\\\") || raw.includes("\0")) {
243
290
  throw error("human_confirmation_ledger_root_invalid");
244
291
  }
@@ -1,3 +1,4 @@
1
+ import { checkDarwinPath, DARWIN_VAULT_FILE, withDarwinVault } from "./hellolynk-darwin-vault.mjs";
1
2
  import {
2
3
  createHash,
3
4
  createHmac,
@@ -261,17 +262,26 @@ async function readProtectionState(statePath) {
261
262
 
262
263
  export function createWindowsLocalDataKeyProvider(options = {}) {
263
264
  const platform = options.platform || process.platform;
264
- if (platform !== "win32") throw codeError("local_data_key_platform_unsupported");
265
+ if (!["darwin", "win32"].includes(platform)) throw codeError("local_data_key_platform_unsupported");
265
266
  const root = assertAbsoluteRoot(options.root);
266
267
  const helperPath = resolve(options.helperPath || WINDOWS_HELPER_PATH);
267
- const invoke = options.invoke || ((command) => invokeWindowsDataKey({ command, root, helperPath }));
268
+ const invoke = options.invoke || (platform === "darwin"
269
+ ? command => withDarwinVault({ root, helperPath:options.helperPath, readOnly:command === "Load", allowMissing:true }, state => {
270
+ if (!state.localDataKey) {
271
+ if (command === "Load") throw codeError("local_data_key_missing");
272
+ state.localDataKey = { keyId:"ldk_" + cryptoRandomBytes(16).toString("base64url"), key:cryptoRandomBytes(32).toString("base64url") };
273
+ }
274
+ return state.localDataKey;
275
+ })
276
+ : command => invokeWindowsDataKey({ command, root, helperPath }));
277
+ const slot = platform === "darwin" ? DARWIN_VAULT_FILE : WINDOWS_KEY_FILE;
268
278
  let current = null;
269
279
  let durableFingerprint = null;
270
280
  const batches = new AsyncLocalStorage();
271
281
 
272
282
  async function rememberDurableSlot() {
273
283
  try {
274
- durableFingerprint = await keySlotFingerprint(root);
284
+ durableFingerprint = await keySlotFingerprint(root, slot);
275
285
  const batch = batches.getStore();
276
286
  if (batch?.active) batch.record = current;
277
287
  } catch (error) {
@@ -287,7 +297,7 @@ export function createWindowsLocalDataKeyProvider(options = {}) {
287
297
  if (batch?.active && current != null && batch.record === current) return;
288
298
  let observed;
289
299
  try {
290
- observed = await keySlotFingerprint(root);
300
+ observed = await keySlotFingerprint(root, slot);
291
301
  } catch (error) {
292
302
  current?.key.fill(0);
293
303
  current = null;
@@ -348,7 +358,7 @@ export function createWindowsLocalDataKeyProvider(options = {}) {
348
358
  });
349
359
  }
350
360
 
351
- async function keySlotFingerprint(root) {
361
+ async function keySlotFingerprint(root, slot) {
352
362
  const rootMetadata = await lstat(root).catch((error) => {
353
363
  if (error?.code === "ENOENT") throw codeError("local_data_key_missing");
354
364
  throw error;
@@ -356,11 +366,15 @@ async function keySlotFingerprint(root) {
356
366
  if (!rootMetadata.isDirectory() || rootMetadata.isSymbolicLink()) {
357
367
  throw codeError("local_data_key_root_invalid");
358
368
  }
359
- const slotPath = join(root, WINDOWS_KEY_FILE);
369
+ const slotPath = join(root, slot);
360
370
  let before;
361
371
  let handle;
362
372
  try {
363
373
  before = await lstat(slotPath);
374
+ if (slot === DARWIN_VAULT_FILE) {
375
+ checkDarwinPath(root, { directory:true, privateMode:true });
376
+ checkDarwinPath(slotPath, { privateMode:true });
377
+ }
364
378
  if (!before.isFile() || before.isSymbolicLink() || before.size < 2 || before.size > MAX_KEY_STORE_BYTES) {
365
379
  throw codeError("local_data_key_store_invalid");
366
380
  }
@@ -370,7 +384,8 @@ async function keySlotFingerprint(root) {
370
384
  throw codeError("local_data_key_store_invalid");
371
385
  }
372
386
  const bytes = await handle.readFile();
373
- return `${opened.dev}:${opened.ino}:${createHash("sha256").update(bytes).digest("hex")}`;
387
+ const permissions = slot === DARWIN_VAULT_FILE ? `${rootMetadata.ctimeMs}:${opened.ctimeMs}:` : "";
388
+ return `${permissions}${opened.dev}:${opened.ino}:${createHash("sha256").update(bytes).digest("hex")}`;
374
389
  } catch (error) {
375
390
  if (error?.code === "ENOENT") throw codeError("local_data_key_missing");
376
391
  throw error;
@@ -1,3 +1,4 @@
1
+ import { DARWIN_HELPER, darwinHelperEnvironment } from "./hellolynk-darwin-vault.mjs";
1
2
  import crypto from "node:crypto";
2
3
  import { spawn } from "node:child_process";
3
4
  import { StringDecoder } from "node:string_decoder";
@@ -583,7 +584,7 @@ export function createWindowsNativeHumanConfirmationLauncher() {
583
584
  await context.deliver?.({ kind: "failure", code: "human_confirmation_timed_out" });
584
585
  return;
585
586
  }
586
- if (process.platform !== "win32") {
587
+ if (!["darwin","win32"].includes(process.platform)) {
587
588
  await context.deliver?.({ kind: "failure", code: "human_confirmation_platform_unsupported" });
588
589
  return;
589
590
  }
@@ -592,12 +593,12 @@ export function createWindowsNativeHumanConfirmationLauncher() {
592
593
  await context.deliver?.({ kind: "failure", code: "human_confirmation_request_too_large" });
593
594
  return;
594
595
  }
595
- await runPowerShellChild(serialized, context);
596
+ await runNativeDialogChild(serialized, context);
596
597
  },
597
598
  });
598
599
  }
599
600
 
600
- async function runPowerShellChild(serialized, context) {
601
+ async function runNativeDialogChild(serialized, context) {
601
602
  await new Promise((resolve) => {
602
603
  let delivered = false;
603
604
  let stdout = "";
@@ -605,13 +606,14 @@ async function runPowerShellChild(serialized, context) {
605
606
  let stdoutBytes = 0;
606
607
  let stderrBytes = 0;
607
608
  let forcedCode = null;
608
- const child = spawn("powershell.exe", [
609
+ const darwin = process.platform === "darwin";
610
+ const child = spawn(darwin ? DARWIN_HELPER : "powershell.exe", darwin ? ["confirm"] : [
609
611
  "-NoLogo", "-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass",
610
612
  "-STA", "-File", POWERSHELL_SCRIPT,
611
613
  ], {
612
614
  windowsHide: true,
613
615
  stdio: ["pipe", "pipe", "pipe"],
614
- env: sanitizeHelloLynkChildEnvironment(process.env),
616
+ env: darwin ? darwinHelperEnvironment() : sanitizeHelloLynkChildEnvironment(process.env),
615
617
  });
616
618
  const finish = async (result) => {
617
619
  if (delivered) return;
@@ -340,7 +340,7 @@ async function assertReadOnlyPath(path, directory, verifyPath) {
340
340
  if (metadata.isSymbolicLink() || (directory ? !metadata.isDirectory() : !metadata.isFile())) {
341
341
  throw codeError(directory ? "host_store_root_invalid" : "host_registry_path_invalid");
342
342
  }
343
- if (process.platform === "win32") {
343
+ if (process.platform === "win32" || (process.platform === "darwin" && verifyPath)) {
344
344
  await verifyPath({ path, directory });
345
345
  }
346
346
  }
@@ -48,7 +48,7 @@ const SAFE_BOOTSTRAP_FAILURE_CODES = new Set([
48
48
  "neutral_trae_work_uncaught_exception",
49
49
  "powershell7_missing",
50
50
  ]);
51
- const SAFE_RELATIVE = /^(?:[A-Za-z0-9][A-Za-z0-9._-]*\/)*[A-Za-z0-9][A-Za-z0-9._-]*\.(?:cjs|cs|css|html|js|json|md|mjs|ps1|vbs)$/;
51
+ const SAFE_RELATIVE = /^(?:[A-Za-z0-9][A-Za-z0-9._-]*\/)*[A-Za-z0-9][A-Za-z0-9._-]*\.(?:bin|cjs|cs|css|html|js|json|md|mjs|ps1|swift|vbs)$/;
52
52
  const REQUIRED_RUNTIME_RECORDS = Object.freeze([
53
53
  "neutral-host-bootstrap.mjs",
54
54
  "neutral-host-release-loader.mjs",
@@ -63,8 +63,14 @@ const REQUIRED_RUNTIME_RECORDS = Object.freeze([
63
63
  "core/hellolynk-neutral-host-process.mjs",
64
64
  "core/hellolynk-neutral-host-workbench-read-model.mjs",
65
65
  "core/hellolynk-workbench-action-grants.mjs",
66
- "platform/windows/hellolynk-host-protected-store.ps1",
67
- "platform/windows/hellolynk-local-data-key.ps1",
66
+ ...(process.platform === "darwin" ? [
67
+ "core/hellolynk-darwin-vault.mjs",
68
+ "core/hellolynk-darwin-host.mjs",
69
+ "platform/darwin/hellolynk-keychain-helper.bin",
70
+ ] : [
71
+ "platform/windows/hellolynk-host-protected-store.ps1",
72
+ "platform/windows/hellolynk-local-data-key.ps1",
73
+ ]),
68
74
  "workbench/contract.mjs",
69
75
  ]);
70
76
 
@@ -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 = 15;
37
+ export const HELLOLYNK_HOST_RELEASE_ORDINAL = 16;
38
38
 
39
39
  const INSTALLATION_LOCK_CONTRACT = "hellolynk-neutral-host-installation-lock-v2";
40
40
  const INSTALLATION_LOCK_MAX_BYTES = 4 * 1024;
@@ -1,3 +1,4 @@
1
+ import { checkDarwinPath, DARWIN_HELPER, invokeDarwinHelper, withDarwinVault } from "./hellolynk-darwin-vault.mjs";
1
2
  import { AsyncLocalStorage } from "node:async_hooks";
2
3
  import { spawn as nodeSpawn } from "node:child_process";
3
4
  import { randomBytes } from "node:crypto";
@@ -260,6 +261,14 @@ export async function runWindowsClientTrustCommand(command, options = {}) {
260
261
  }
261
262
 
262
263
  export async function prepareWindowsClientRoot(clientRoot, options = {}) {
264
+ if ((options.platform || process.platform) === "darwin") {
265
+ checkDarwinPath(clientRoot, { directory: true, create: true, privateMode: true });
266
+ for (const sub of ["profiles", "credentials", "e2ee-private-keys", "status", "host", "host/runtime", "releases"]) {
267
+ checkDarwinPath(join(clientRoot, sub), { directory: true, create: true, privateMode: true });
268
+ }
269
+ await invokeDarwinHelper(options.helperPath || DARWIN_HELPER, ["verify-acl", clientRoot]);
270
+ return "prepared";
271
+ }
263
272
  return runWindowsClientTrustCommand("PrepareRoot", {
264
273
  ...options,
265
274
  clientRoot,
@@ -272,6 +281,14 @@ export async function secureWindowsClientReleaseTree(clientRoot, releaseId, opti
272
281
  if (!/^(?!\.{1,2}$)[A-Za-z0-9.][A-Za-z0-9_.-]{2,191}$/.test(acceptedReleaseId)) {
273
282
  throw new HelloLynkCredentialStoreError("client_release_path_invalid");
274
283
  }
284
+ if ((options.platform || process.platform) === "darwin") {
285
+ checkDarwinPath(clientRoot, { directory: true, privateMode: true });
286
+ checkDarwinPath(join(clientRoot, "releases"), { directory: true, privateMode: true });
287
+ const release = join(clientRoot, "releases", acceptedReleaseId);
288
+ checkDarwinPath(release, { directory: true });
289
+ await invokeDarwinHelper(options.helperPath || DARWIN_HELPER, [options.verifyOnly ? "verify-tree" : "secure-tree", release]);
290
+ return options.verifyOnly ? "verified" : "secured";
291
+ }
275
292
  return runWindowsClientTrustCommand(options.verifyOnly === true ? "VerifyReleaseTree" : "SecureReleaseTree", {
276
293
  ...options,
277
294
  clientRoot,
@@ -460,24 +477,61 @@ export function createPlatformCredentialStore(options = {}) {
460
477
  }
461
478
 
462
479
  if (platform === "darwin") {
463
- const helper = options.helperPath || join(
464
- homedir(), "Library", "Application Support", "HelloLynk", "bin", "hellolynk-keychain-helper",
465
- );
466
- const invoke = (command, profileId, stdin) => run(
467
- helper,
468
- [command.toLowerCase(), profileId],
469
- { ...runnerOptions, stdin },
470
- );
480
+ const clientRoot = options.clientRoot || (options.storeRoot ? windowsClientRootForStore(options.storeRoot)
481
+ : join(homedir(), "Library", "Application Support", "HelloLynk"));
482
+ if (clientRoot !== resolve(clientRoot) || clientRoot === "/") throw new HelloLynkCredentialStoreError("client_root_path_invalid");
483
+ const credentials = join(clientRoot, "credentials");
484
+ const privateKeys = join(clientRoot, "e2ee-private-keys");
485
+ const root = options.storeRoot || credentials;
486
+ const e2eeRoot = options.e2eeStoreRoot || privateKeys;
487
+ if (root !== clientRoot && root !== credentials) throw new HelloLynkCredentialStoreError("credential_store_root_invalid");
488
+ if (e2eeRoot !== privateKeys) throw new HelloLynkCredentialStoreError("e2ee_private_key_store_root_invalid");
489
+ function entry(location, section, profile, operation, value) {
490
+ const slot = section + ":" + assertCredentialProfileId(profile);
491
+ return withDarwinVault({ root: location, helperPath: options.helperPath, readOnly: operation === "load", allowMissing:true }, data => {
492
+ if (operation === "save") data[slot] = value;
493
+ if (operation === "remove") delete data[slot];
494
+ return data[slot] ?? null;
495
+ });
496
+ }
471
497
  return {
472
498
  platform,
473
- async save(profile, credential) {
474
- await invoke("Store", assertCredentialProfileId(profile), assertCredential(credential));
475
- },
499
+ async save(profile, credential) { await entry(root, "device", profile, "save", assertCredential(credential)); },
476
500
  async load(profile) {
477
- return assertCredential(await invoke("Load", assertCredentialProfileId(profile), ""));
501
+ const value = await entry(root, "device", profile, "load");
502
+ if (value === null) throw new HelloLynkCredentialStoreError("device_credential_missing");
503
+ return assertCredential(value);
478
504
  },
479
- async remove(profile) {
480
- await invoke("Remove", assertCredentialProfileId(profile), "");
505
+ async remove(profile) { await entry(root, "device", profile, "remove"); },
506
+ runtimeSessions: {
507
+ async save(profile, session) { await entry(root, "session", profile, "save", assertRuntimeSession(session)); },
508
+ async load(profile) {
509
+ const value = await entry(root, "session", profile, "load");
510
+ return value === null ? null : assertRuntimeSession(value);
511
+ },
512
+ async remove(profile) { await entry(root, "session", profile, "remove"); },
513
+ async withLock(profile, operation) {
514
+ const id = assertCredentialProfileId(profile);
515
+ if (typeof operation !== "function") throw new HelloLynkCredentialStoreError("runtime_session_lock_operation_invalid");
516
+ checkDarwinPath(root, { directory:true, create:true, privateMode:true });
517
+ return withRuntimeSessionFileLock(root, id, operation, options.runtimeSessionLockDependencies, options.timeoutMs);
518
+ },
519
+ },
520
+ directTextPrivateKeys: {
521
+ async save(profile, material) { await entry(e2eeRoot, "keys", profile, "save", encodeDirectTextPrivateKeyMaterial(material)); },
522
+ async load(profile) {
523
+ const value = await entry(e2eeRoot, "keys", profile, "load");
524
+ return value === null ? null : decodeDirectTextPrivateKeyMaterial(value);
525
+ },
526
+ async remove(profile) { await entry(e2eeRoot, "keys", profile, "remove"); },
527
+ async preload(profiles) {
528
+ const ids = [...new Set((Array.isArray(profiles) ? profiles : []).map(assertCredentialProfileId))];
529
+ if (!ids.length) return;
530
+ if (ids.length > MAX_E2EE_BATCH_PROFILES) throw new HelloLynkCredentialStoreError("e2ee_private_key_batch_invalid");
531
+ await withDarwinVault({root:e2eeRoot, helperPath:options.helperPath, readOnly:true, allowMissing:true}, data => {
532
+ for (const id of ids) if (data["keys:" + id] !== undefined) decodeDirectTextPrivateKeyMaterial(data["keys:" + id]);
533
+ });
534
+ },
481
535
  },
482
536
  };
483
537
  }
@@ -2,6 +2,7 @@ import fs from 'node:fs/promises';
2
2
  import path from 'node:path';
3
3
  import crypto from 'node:crypto';
4
4
  import { execFile } from 'node:child_process';
5
+ import { homedir } from 'node:os';
5
6
  import { promisify } from 'node:util';
6
7
 
7
8
  const execute = promisify(execFile);
@@ -27,7 +28,7 @@ const equal = (a, b) => process.platform === 'win32' ? a.toLowerCase() === b.toL
27
28
  // recent successful binding, even when its command line names only the cache.
28
29
  // A checkout/uninstalled fixture has no immutable release to retain.
29
30
  export async function recordCodexReleaseProcess(packageRoot) {
30
- if (process.platform !== 'win32' || !path.isAbsolute(packageRoot || '')
31
+ if (!['win32', 'darwin'].includes(process.platform) || !path.isAbsolute(packageRoot || '')
31
32
  || path.basename(path.dirname(packageRoot)) !== 'releases' || !/^codex-[a-f0-9]{64}$/.test(path.basename(packageRoot))) return;
32
33
  try {
33
34
  const root = await rootPath(path.dirname(packageRoot));
@@ -70,6 +71,10 @@ async function regularTree(target) {
70
71
  // older Host may still import files after installation; deletion waits for the
71
72
  // next successful install instead of stopping any user process.
72
73
  export async function runningReleaseNames(releasesRoot) {
74
+ if (process.platform === 'darwin') {
75
+ const pattern = darwinReleasePattern(await rootPath(releasesRoot), releasesRoot);
76
+ return darwinReleaseNames(await darwinPs('command='), pattern);
77
+ }
73
78
  if (process.platform !== 'win32') throw failure('release_retention_process_check_unavailable');
74
79
  const root = await rootPath(releasesRoot);
75
80
  const literal = `${root}${path.sep}`.replaceAll("'", "''");
@@ -89,8 +94,21 @@ export async function runningReleaseNames(releasesRoot) {
89
94
  // with a positive pid are listed, and command lines never leave PowerShell.
90
95
  export async function entryProcessList(client, releasesRoot, pids = []) {
91
96
  if (!ENTRY[client] && client !== 'codex') return [];
92
- if (process.platform !== 'win32') throw failure('release_retention_process_check_unavailable');
93
97
  if (!Array.isArray(pids) || pids.some(pid => !Number.isSafeInteger(pid) || pid <= 0)) throw failure('release_retention_process_check_invalid');
98
+ if (process.platform === 'darwin') {
99
+ const root = await rootPath(releasesRoot);
100
+ const stdout = await darwinPs('pid=,ppid=,lstart=,command=');
101
+ if (client === 'codex') return parseDarwinCodexEntryProcesses(stdout, pids);
102
+ const entries = [...new Set([root, path.resolve(releasesRoot)])]
103
+ .map(value => path.join(path.dirname(value), ...ENTRY[client]).replace(/[.*+?^${}()|[\]\\]/g, '\\$&'));
104
+ const pattern = new RegExp(`(?:^|["\\s])(?:${entries.join('|')})(?=["\\s]|$)`, 'u');
105
+ const wanted = new Set(pids);
106
+ return [...parseDarwinProcesses(stdout).values()].flatMap(({ pid, startedAt, command }) => {
107
+ const entry = pattern.test(command);
108
+ return entry || wanted.has(pid) ? [{ pid, startedAt, entry }] : [];
109
+ });
110
+ }
111
+ if (process.platform !== 'win32') throw failure('release_retention_process_check_unavailable');
94
112
  const root = await rootPath(releasesRoot);
95
113
  // This installation's entry only, in its given and canonical spellings:
96
114
  // any case, either slash, quoted or not.
@@ -112,6 +130,50 @@ export async function entryProcessList(client, releasesRoot, pids = []) {
112
130
  return list;
113
131
  }
114
132
 
133
+ async function darwinPs(fields) {
134
+ const { stdout } = await execute('/bin/ps', ['-ww', '-axo', fields], {
135
+ env: { LC_ALL: 'C', TZ: 'UTC0' }, timeout: 15000, maxBuffer: 4 * 1024 * 1024,
136
+ });
137
+ return stdout;
138
+ }
139
+
140
+ // Keep command lines inside the process probe. Only a PID, its start time and
141
+ // whether it might still be serving the Codex cache reach the retention logic.
142
+ function parseDarwinProcesses(stdout) {
143
+ const processes = new Map();
144
+ for (const line of stdout.trim().split('\n')) {
145
+ const fields = /^\s*(\d+)\s+(\d+)\s+(\w{3}\s+\w{3}\s+\d{1,2}\s+\d{2}:\d{2}:\d{2}\s+\d{4})\s+(.+)$/u.exec(line);
146
+ if (!fields) throw failure('release_retention_process_check_invalid');
147
+ const [, pid, parent, started, command] = fields;
148
+ const startedAt = Date.parse(`${started} UTC`);
149
+ processes.set(Number(pid), { pid: Number(pid), parent: Number(parent), startedAt: Number.isFinite(startedAt) ? startedAt : null, command });
150
+ }
151
+ return processes;
152
+ }
153
+
154
+ export function parseDarwinCodexEntryProcesses(stdout, pids = []) {
155
+ const processes = parseDarwinProcesses(stdout);
156
+ const wanted = new Set(pids);
157
+ const proxy = /(?:^|["\s/])(?:\.\/)?server\/mcp-proxy\.mjs(?=["\s]|$)/u;
158
+ const codexApp = /(?:^|["/])(?:Codex|ChatGPT)\.app\/Contents\//iu;
159
+ const codexCli = /^(?:"?[^"\s]*\/)?codex(?=["\s]|$)/iu;
160
+ const result = [];
161
+ for (const { pid, parent, startedAt, command } of processes.values()) {
162
+ let entry = false;
163
+ if (proxy.test(command) && /(?:^|\/)node(?=["\s]|$)/u.test(command)) {
164
+ entry = /\/cua_node\/bin\/node(?=["\s]|$)/u.test(command)
165
+ || /\/hellolynk-codex\/server\/mcp-proxy\.mjs(?=["\s]|$)/u.test(command);
166
+ const seen = new Set([pid]);
167
+ for (let ancestor = processes.get(parent); !entry && ancestor && !seen.has(ancestor.pid); ancestor = processes.get(ancestor.parent)) {
168
+ entry = codexApp.test(ancestor.command) || codexCli.test(ancestor.command);
169
+ seen.add(ancestor.pid);
170
+ }
171
+ }
172
+ if (entry || wanted.has(pid)) result.push({ pid, startedAt, entry });
173
+ }
174
+ return result;
175
+ }
176
+
115
177
  // Releases named by live entry records, and whether a process started through
116
178
  // the stable entry has no matching record (one from before records existed).
117
179
  // Records of exited or reused pids are removed, so a release can go at the
@@ -149,6 +211,33 @@ async function entryReleases(releasesRoot, root, client, pattern, dependencies)
149
211
  // A stopped on-demand task is still a configuration reference. Keeping only
150
212
  // live processes can delete its executable before the next client startup.
151
213
  export async function configuredReleaseNames(releasesRoot) {
214
+ if (process.platform === 'darwin') {
215
+ const root = await rootPath(releasesRoot);
216
+ const pattern = darwinReleasePattern(root, releasesRoot);
217
+ const names = new Set();
218
+ const readAgent = async file => {
219
+ const { stdout } = await execute('/usr/bin/plutil', ['-convert', 'json', '-o', '-', file],
220
+ { timeout: 15000, maxBuffer: 256 * 1024 });
221
+ const { Program, WorkingDirectory, ProgramArguments } = JSON.parse(stdout);
222
+ for (const value of [Program, WorkingDirectory, ...(ProgramArguments || [])]) {
223
+ if (typeof value === 'string') for (const name of darwinReleaseNames(value, pattern)) names.add(name);
224
+ }
225
+ };
226
+ // A receiver set to start on demand keeps its definition outside LaunchAgents.
227
+ const definition = path.join(path.dirname(root), 'host', 'claude-native-service.plist');
228
+ if (await fs.lstat(definition).catch(error => { if (error.code === 'ENOENT') return null; throw error; })) {
229
+ await readAgent(definition);
230
+ }
231
+ for (const directory of [path.join(homedir(), 'Library', 'LaunchAgents'), '/Library/LaunchAgents', '/Library/LaunchDaemons']) {
232
+ let entries;
233
+ try { entries = await fs.readdir(directory); }
234
+ catch (error) { if (error.code === 'ENOENT') continue; throw error; }
235
+ for (const entry of entries.filter(name => /hellolynk/i.test(name) && name.endsWith('.plist'))) {
236
+ await readAgent(path.join(directory, entry));
237
+ }
238
+ }
239
+ return [...names].sort();
240
+ }
152
241
  if (process.platform !== 'win32') throw failure('release_retention_configuration_check_unavailable');
153
242
  const root = await rootPath(releasesRoot);
154
243
  const prefixes = [...new Set([root, path.resolve(releasesRoot)])].map(value => (value + path.sep).replace(/[.*+?^${}()|[\]\\]/g, '\\$&'));
@@ -164,6 +253,16 @@ export async function configuredReleaseNames(releasesRoot) {
164
253
  return names;
165
254
  }
166
255
 
256
+ function darwinReleasePattern(root, logicalRoot) {
257
+ const prefixes = [...new Set([root, path.resolve(logicalRoot)])]
258
+ .map(value => (value + path.sep).replace(/[.*+?^${}()|[\]\\]/g, '\\$&'));
259
+ return new RegExp('(?:' + prefixes.join('|') + ')((?:' + clients.join('|') + ')-[a-f0-9]{64})(?=[/"\\s]|$)', 'g');
260
+ }
261
+
262
+ function darwinReleaseNames(text, pattern) {
263
+ return [...new Set([...text.matchAll(pattern)].map(match => match[1]))].sort();
264
+ }
265
+
167
266
  // Call only after the caller's complete adoption transaction succeeds, while
168
267
  // holding its install/account lock. Never call from candidate staging or a
169
268
  // rollback. The receipt records adoption order, not directory timestamps.
@@ -60,7 +60,7 @@ export function validateWorkbenchConnectionInput(value) {
60
60
  export function createWorkbenchConnection(options) {
61
61
  if (!CLIENTS.has(options?.client) || !path.isAbsolute(options?.releaseRoot || "")) throw code("workbench_connect_configuration_invalid");
62
62
  const env = options.env || process.env;
63
- const storeRoot = options.storeRoot || env.HELLOLYNK_NEUTRAL_STORE_ROOT || defaultNeutralHostStoreRoot("win32", env);
63
+ const storeRoot = options.storeRoot || env.HELLOLYNK_NEUTRAL_STORE_ROOT || defaultNeutralHostStoreRoot(process.platform, env);
64
64
  if (!path.isAbsolute(storeRoot)) throw code("workbench_connect_configuration_invalid");
65
65
  let busy = false;
66
66
  const accounts = createClientAccountActions(options);
@@ -87,7 +87,7 @@ export function createWorkbenchAccountActions(options) {
87
87
  }
88
88
 
89
89
  async function connectAccount(client, sourceRoot, storeRoot, input) {
90
- if (process.platform !== "win32") throw code("workbench_connect_windows_only", 400);
90
+ if (!["win32", "darwin"].includes(process.platform)) throw code("workbench_connect_platform_unsupported", 400);
91
91
  if (!CLIENTS.has(client) || !path.isAbsolute(sourceRoot) || !path.isAbsolute(storeRoot)
92
92
  || path.basename(storeRoot).toLowerCase() !== "host") throw code("workbench_connect_configuration_invalid");
93
93
  const value = validateWorkbenchConnectionInput(input);
@@ -181,7 +181,9 @@ async function retainRelease(binding, currentRoot, client) {
181
181
  const destination = path.resolve(stage, relative);
182
182
  if (!destination.startsWith(`${stage}${path.sep}`)) throw code("workbench_connect_release_invalid");
183
183
  await fs.mkdir(path.dirname(destination), { recursive: true, mode: 0o700 });
184
- await fs.writeFile(destination, await fs.readFile(path.join(sourceRoot, relative)), { flag: "wx", mode: 0o600 });
184
+ await fs.writeFile(destination, await fs.readFile(path.join(sourceRoot, relative)), {
185
+ flag: "wx", mode: relative.endsWith(".bin") ? 0o700 : 0o600,
186
+ });
185
187
  }
186
188
  await verifyPinnedNeutralHostRuntime({ ...binding, releaseRoot: stage });
187
189
  await secureWindowsClientReleaseTree(currentRoot, stageId);
@@ -10,7 +10,7 @@ import { startHelloLynkNeutralHostAssembly } from "./core/hellolynk-neutral-host
10
10
 
11
11
  export async function createNeutralHostReleaseRuntime() {
12
12
  return Object.freeze({
13
- defaultStoreRoot: (env) => defaultNeutralHostStoreRoot("win32", env),
13
+ defaultStoreRoot: (env) => defaultNeutralHostStoreRoot(process.platform, env),
14
14
  createInstallationStore: (options) => createNeutralHostInstallationStore(options),
15
15
  createProtectedStore: (options) => createHostProtectedStore(options),
16
16
  run: async ({ root, binding }) => runProductionHost(root, binding),