@hellolynk-ai/codex-plugin 0.2.100 → 0.2.102-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 (47) hide show
  1. package/README.md +17 -17
  2. package/bin/hellolynk-client-setup.mjs +79 -11
  3. package/hellolynk-client-pack-manifest.json +111 -71
  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-client-profile.mjs +25 -2
  11. package/plugins/hellolynk-codex/runtime/bootstrap/core/hellolynk-darwin-host.mjs +256 -0
  12. package/plugins/hellolynk-codex/runtime/bootstrap/core/hellolynk-darwin-vault.mjs +153 -0
  13. package/plugins/hellolynk-codex/runtime/bootstrap/core/hellolynk-host-protected-store.mjs +20 -8
  14. package/plugins/hellolynk-codex/runtime/bootstrap/core/hellolynk-neutral-host-installation-store.mjs +3 -7
  15. package/plugins/hellolynk-codex/runtime/bootstrap/core/hellolynk-platform-credential-store.mjs +68 -14
  16. package/plugins/hellolynk-codex/runtime/bootstrap/platform/darwin/hellolynk-keychain-helper.bin +0 -0
  17. package/plugins/hellolynk-codex/runtime/bootstrap/platform/darwin/hellolynk-keychain-helper.swift +149 -0
  18. package/plugins/hellolynk-codex/runtime/connect-activate.mjs +5 -1
  19. package/plugins/hellolynk-codex/runtime/core/hellolynk-client-account.mjs +1 -1
  20. package/plugins/hellolynk-codex/runtime/core/hellolynk-client-profile.mjs +25 -2
  21. package/plugins/hellolynk-codex/runtime/core/hellolynk-client-update-entry.mjs +7 -3
  22. package/plugins/hellolynk-codex/runtime/core/hellolynk-client-update-runtime.mjs +24 -8
  23. package/plugins/hellolynk-codex/runtime/core/hellolynk-client-update.mjs +23 -9
  24. package/plugins/hellolynk-codex/runtime/core/hellolynk-darwin-host.mjs +256 -0
  25. package/plugins/hellolynk-codex/runtime/core/hellolynk-darwin-vault.mjs +153 -0
  26. package/plugins/hellolynk-codex/runtime/core/hellolynk-host-protected-store.mjs +20 -8
  27. package/plugins/hellolynk-codex/runtime/core/hellolynk-human-confirmation-ledger.mjs +53 -6
  28. package/plugins/hellolynk-codex/runtime/core/hellolynk-local-data-protection.mjs +22 -7
  29. package/plugins/hellolynk-codex/runtime/core/hellolynk-native-addressing-tools.cjs +1 -1
  30. package/plugins/hellolynk-codex/runtime/core/hellolynk-native-human-confirmation-broker.mjs +7 -5
  31. package/plugins/hellolynk-codex/runtime/core/hellolynk-neutral-host-installation-store.mjs +3 -7
  32. package/plugins/hellolynk-codex/runtime/core/hellolynk-neutral-host-launcher.mjs +9 -3
  33. package/plugins/hellolynk-codex/runtime/core/hellolynk-neutral-host-process.mjs +1 -1
  34. package/plugins/hellolynk-codex/runtime/core/hellolynk-platform-credential-store.mjs +68 -14
  35. package/plugins/hellolynk-codex/runtime/core/hellolynk-release-retention.mjs +116 -19
  36. package/plugins/hellolynk-codex/runtime/core/hellolynk-workbench-connection.mjs +5 -3
  37. package/plugins/hellolynk-codex/runtime/neutral-host-release-entry.mjs +1 -1
  38. package/plugins/hellolynk-codex/runtime/neutral-host-release-loader.mjs +1 -1
  39. package/plugins/hellolynk-codex/runtime/neutral-host-release-manifest.json +59 -27
  40. package/plugins/hellolynk-codex/runtime/neutral-host-release-pin.json +1 -1
  41. package/plugins/hellolynk-codex/runtime/platform/darwin/hellolynk-keychain-helper.bin +0 -0
  42. package/plugins/hellolynk-codex/runtime/platform/darwin/hellolynk-keychain-helper.swift +149 -0
  43. package/plugins/hellolynk-codex/server/codex-neutral-production.mjs +17 -9
  44. package/plugins/hellolynk-codex/server/mcp-server.mjs +1193 -1193
  45. package/plugins/hellolynk-codex/server/native-conversation-directory.mjs +33 -33
  46. package/plugins/hellolynk-codex/server/workbench-runtime.mjs +42 -42
  47. package/tools/hellolynk-activate-neutral-host-clients.mjs +5 -1
@@ -0,0 +1,256 @@
1
+ import { isDeepStrictEqual as equal } from "node:util";
2
+ import { join } from "node:path";
3
+ import { checkDarwinPath, vaultError, withDarwinVault } from "./hellolynk-darwin-vault.mjs";
4
+
5
+ const permanent = 253402300799999;
6
+ const month = 30 * 86400000;
7
+ function fail(code) { throw vaultError(code); }
8
+ function fieldsMatch(a, b, fields) { return fields.every(field => equal(a[field], b[field])); }
9
+ function sameOwner(a, b) { return fieldsMatch(a, b, ["principalId", "hostInstallationId"]); }
10
+ function used(records, field, id) { return records.some(r => r[field].toLowerCase() === id.toLowerCase()); }
11
+ function unique(records, missing, ambiguous) {
12
+ if (records.length !== 1) fail(records.length ? ambiguous : missing);
13
+ return records[0];
14
+ }
15
+ function checkBindingPaths(b) {
16
+ try {
17
+ checkDarwinPath(b.runtimeRoot, { directory: true });
18
+ checkDarwinPath(b.releaseRoot, { directory: true });
19
+ if (checkDarwinPath(b.profileManifestPath).size > 65536 ||
20
+ checkDarwinPath(join(b.releaseRoot, b.runnerRelativePath)).size > 1048576) fail("neutral_codex_launch_manifest_invalid");
21
+ } catch { fail("neutral_codex_launch_manifest_invalid"); }
22
+ }
23
+ function capMatches(cap, b) {
24
+ return cap.state === "active" && fieldsMatch(cap, b,
25
+ ["region", "principalId", "delegateId", "hostInstallationId", "adapterId", "version", "scopes"]);
26
+ }
27
+ function addCapability(s, r) {
28
+ if (used(s.caps, "capabilityId", r.capabilityId)) fail("host_capability_id_conflict");
29
+ s.caps.push(r);
30
+ }
31
+ function revokeCapability(s, r) {
32
+ if (!r) fail("host_capability_missing");
33
+ if (r.state === "active") {
34
+ const revokedAt = Date.now();
35
+ if (revokedAt < r.issuedAt) fail("host_capability_state_invalid");
36
+ // Keep the identifier spent while discarding the retired secret.
37
+ r = s.caps[s.caps.indexOf(r)] = { capabilityId: r.capabilityId, issuedAt: r.issuedAt, state: "revoked", revokedAt };
38
+ }
39
+ return { capabilityId: r.capabilityId, revokedAt: r.revokedAt, state: "revoked" };
40
+ }
41
+ function codexBinding(s, command, input, oldId) {
42
+ const b = s.codex;
43
+ if (command === "ResolveCodexNeutralBinding" || command === "VerifyCodexNeutralBinding") {
44
+ if (!b) fail("neutral_codex_binding_configuration_required");
45
+ checkBindingPaths(b);
46
+ const active = s.caps.filter(c => c.adapterId === "codex" && c.state === "active");
47
+ const cap = unique(active.filter(c => capMatches(c, b)),
48
+ active.length ? "neutral_codex_capability_binding_mismatch" : "neutral_codex_capability_missing", "neutral_codex_capability_ambiguous");
49
+ if (command.startsWith("Resolve")) return { binding: b, record: cap };
50
+ const {secret, ...record} = cap;
51
+ return { binding: b, record };
52
+ }
53
+ if (command === "SaveCodexNeutralBinding" || command === "RemoveCodexNeutralBinding") {
54
+ checkBindingPaths(input);
55
+ if (command.startsWith("Remove")) {
56
+ if (!equal(b, input)) fail("neutral_codex_binding_invalid");
57
+ s.codex = null;
58
+ return { removed: true };
59
+ }
60
+ if (b && !equal(b, input)) fail("neutral_codex_binding_invalid");
61
+ s.codex = input;
62
+ return { binding: input };
63
+ }
64
+ const { previousBinding: previous, replacementBinding: next, replacementRecord } = input;
65
+ checkBindingPaths(previous);
66
+ checkBindingPaths(next);
67
+ if (!fieldsMatch(previous, next, ["contract","region","principalId","delegateId","hostInstallationId","profileId","adapterId","version","runtimeRoot"])) {
68
+ fail("neutral_codex_capability_binding_mismatch");
69
+ }
70
+ if (!equal(b, previous)) fail("neutral_codex_binding_invalid");
71
+ const active = s.caps.filter(c => capMatches(c, next));
72
+ if (command === "ReplaceCodexNeutralBinding") {
73
+ if (!equal(previous.scopes, next.scopes)) fail("neutral_codex_capability_binding_mismatch");
74
+ if (active.length !== 1) fail("neutral_codex_capability_ambiguous");
75
+ s.codex = next;
76
+ return { binding: next };
77
+ }
78
+ if (command !== "MigrateCodexNeutralBinding") fail("host_protected_store_command_invalid");
79
+ const old = s.caps.find(c => c.capabilityId === oldId);
80
+ if (!old || !capMatches(old, previous) || !capMatches(replacementRecord, next)) fail("neutral_codex_capability_binding_mismatch");
81
+ const other = active.filter(c => c.capabilityId !== oldId);
82
+ if (other.length > 1) fail("neutral_codex_capability_ambiguous");
83
+ if (!other.length) addCapability(s, replacementRecord);
84
+ revokeCapability(s, old);
85
+ s.codex = next;
86
+ return { binding: next, record: other[0] || replacementRecord, replacedCapabilityId: oldId };
87
+ }
88
+
89
+ function retireSession(s, r) {
90
+ s.sessions.splice(s.sessions.indexOf(r), 1);
91
+ s.retiredSessions.push(r.sessionId.toLowerCase());
92
+ }
93
+ function revokeCredential(s, r, timestamp) {
94
+ if (timestamp < r.issuedAt) fail("host_transport_credential_revoke_timestamp_invalid");
95
+ const sessions = s.sessions.filter(session => session.credentialId === r.credentialId);
96
+ if (r.state === "active") {
97
+ sessions.forEach(session => retireSession(s, session));
98
+ const { credentialId, issuedAt, principalId, hostInstallationId } = r;
99
+ r = s.credentials[s.credentials.indexOf(r)] = { credentialId, issuedAt, principalId, hostInstallationId, state:"revoked", revokedAt:timestamp };
100
+ }
101
+ return { credentialId:r.credentialId, state:"revoked", revokedAt:r.revokedAt, cascadedSessionCount:sessions.length };
102
+ }
103
+ function exactFields(value, fields) {
104
+ return value && typeof value === "object" && equal(Object.keys(value).sort(), fields.sort());
105
+ }
106
+ function checkEnrollment(value, record) {
107
+ const a = value?.approval, approved = a?.hostTransportCredential;
108
+ if (!exactFields(value, ["baseUrl","hellolynkId","hostId","deviceLabel","requestId","proof","approval","intents"])
109
+ || !exactFields(a, ["status","principalId","hellolynkId","region","hostId","hostTransportCredential"])
110
+ || !exactFields(approved, ["credentialId","credential","createdAt","expiresAt"])
111
+ || a.status !== "approved" || a.principalId !== record.principalId || a.hostId !== record.hostInstallationId
112
+ || value.hostId !== a.hostId || a.hellolynkId !== value.hellolynkId || approved.credentialId !== record.credentialId
113
+ || approved.credential !== record.secret || Date.parse(approved.createdAt) !== record.issuedAt
114
+ || Date.parse(approved.expiresAt) !== record.expiresAt) fail("device_enrollment_transport_approval_mismatch");
115
+ if (record.expiresAt <= Date.now()) fail("host_transport_credential_expired");
116
+ }
117
+
118
+ function transport(s, q, input) {
119
+ const {command, timestamp} = q;
120
+ const id = q.credentialId || input?.credentialId;
121
+ const credential = s.credentials.find(r => r.credentialId === id);
122
+ const owned = query => s.credentials.filter(r => r.state === "active" && sameOwner(r, query));
123
+ if (command === "SaveTransportCredential" || command === "SaveEnrollmentTransportCredential") {
124
+ const upgrade = command === "SaveEnrollmentTransportCredential";
125
+ if (upgrade) checkEnrollment(s.enrollment.value, input);
126
+ if (credential) {
127
+ if (!equal(credential,input)) fail("host_transport_credential_immutable_conflict");
128
+ } else {
129
+ if (used(s.credentials, "credentialId", id)) fail("host_transport_credential_immutable_conflict");
130
+ const old = owned(input);
131
+ if (old.length > 1 || (old.length && !upgrade)) fail("host_transport_credential_ambiguous");
132
+ if (old.length) revokeCredential(s,old[0],Date.now());
133
+ s.credentials.push(input);
134
+ }
135
+ return { record: input };
136
+ }
137
+ if (command === "VerifyTransportCredentialLifetimeMigration" || command === "MigrateTransportCredentialLifetime") {
138
+ const record = unique(owned(q), "host_transport_migration_credential_inactive_or_ambiguous", "host_transport_migration_credential_inactive_or_ambiguous");
139
+ if (record.expiresAt !== permanent && record.expiresAt - record.issuedAt !== month) fail("host_transport_migration_lifetime_invalid");
140
+ if (command.startsWith("Verify")) return { record };
141
+ if (record.expiresAt-record.issuedAt !== month || !equal(record,input.previous)) fail("host_transport_migration_compare_failed");
142
+ const proof = input.proof;
143
+ if (proof.credentialId !== record.credentialId || proof.hostId !== q.hostInstallationId || proof.principalId !== q.principalId
144
+ || Date.parse(proof.credentialCreatedAt) !== record.issuedAt || Date.parse(proof.credentialExpiresAt) !== permanent) fail("host_transport_migration_proof_invalid");
145
+ record.expiresAt = permanent;
146
+ return { credentialId: record.credentialId, expiresAt: permanent, status:"migrated" };
147
+ }
148
+ if (command === "LoadTransportCredential" || command === "RevokeTransportCredential") {
149
+ if (command.startsWith("Load") && credential?.state !== "active") fail("host_transport_credential_inactive");
150
+ if (!credential) fail("host_transport_credential_missing");
151
+ if (!sameOwner(credential,q)) fail("host_transport_credential_owner_mismatch");
152
+ if (command.startsWith("Revoke")) return revokeCredential(s,credential,timestamp);
153
+ if (credential.expiresAt <= timestamp) fail("host_transport_credential_expired");
154
+ return { record: credential };
155
+ }
156
+ const sessionId = q.sessionId || input?.sessionId;
157
+ const session = s.sessions.find(r => r.sessionId === sessionId);
158
+ if (command === "SaveTransportSession" || command === "ReplaceTransportSession") {
159
+ if (credential?.state !== "active") fail("host_transport_credential_inactive");
160
+ if (!sameOwner(credential,input) || credential.expiresAt <= timestamp || input.expiresAt > credential.expiresAt) fail("host_transport_session_credential_mismatch");
161
+ if (s.retiredSessions.includes(sessionId.toLowerCase())) fail("host_transport_session_retired");
162
+ if (session) {
163
+ if (!equal(session,input)) fail("host_transport_session_immutable_conflict");
164
+ } else {
165
+ if (used(s.sessions, "sessionId", sessionId)) fail("host_transport_session_duplicate");
166
+ const previous = s.sessions.filter(r => sameOwner(r,input));
167
+ if (previous.length && command.startsWith("Save")) fail("host_transport_session_ambiguous");
168
+ previous.forEach(r => retireSession(s,r));
169
+ s.sessions.push(input);
170
+ }
171
+ return { record: input };
172
+ }
173
+ if (command === "LoadTransportSession" || command === "RemoveTransportSession") {
174
+ if (session && !sameOwner(session,q)) fail("host_transport_session_owner_mismatch");
175
+ if (command.startsWith("Remove")) {
176
+ if (session) retireSession(s,session);
177
+ return { sessionId, removed: Boolean(session) };
178
+ }
179
+ if (!session) fail("host_transport_session_invalid");
180
+ const parent = s.credentials.find(c => c.credentialId === session.credentialId);
181
+ if (parent?.state !== "active" || parent.expiresAt <= timestamp) fail("host_transport_credential_inactive");
182
+ if (session.expiresAt <= timestamp) fail("host_transport_session_expired");
183
+ return { record: session };
184
+ }
185
+ if (command === "ResolveTransport" || command === "VerifyTransport") {
186
+ const c = unique(owned(q),"host_transport_credential_required","host_transport_credential_ambiguous");
187
+ if (c.expiresAt <= timestamp) fail("host_transport_credential_expired");
188
+ const sessions = s.sessions.filter(r => r.credentialId === c.credentialId && sameOwner(r,q));
189
+ if (sessions.length > 1) fail("host_transport_session_ambiguous");
190
+ let selected = sessions[0] || null;
191
+ if (selected && selected.expiresAt <= timestamp) {
192
+ if (command.startsWith("Resolve")) retireSession(s,selected);
193
+ selected = null;
194
+ }
195
+ if (command.startsWith("Resolve")) return { credential:c, session:selected };
196
+ const publicFields = ({credentialId,principalId,hostInstallationId,expiresAt}) => ({credentialId,principalId,hostInstallationId,expiresAt});
197
+ return { credential:publicFields(c), session:selected && {...publicFields(selected),sessionId:selected.sessionId} };
198
+ }
199
+ fail("host_protected_store_command_invalid");
200
+ }
201
+
202
+ function emptyHost(enrollment = { contract:"hellolynk-device-enrollment-v1", revision:0, value:null }) {
203
+ return { caps:[], credentials:[], sessions:[], retiredSessions:[], codex:null, enrollment };
204
+ }
205
+
206
+ // On Windows the enrollment file exists independently of the capability file.
207
+ // An unclaimed Mac Host may have only its enrollment in the shared vault. Read
208
+ // its authenticated state without creating, repairing or replacing anything.
209
+ export function verifyDarwinUnclaimedAuthorization(root) {
210
+ return withDarwinVault({ root, readOnly:true }, document => {
211
+ const enrollment = document?.host?.enrollment;
212
+ if (!exactFields(enrollment, ["contract", "revision", "value"])
213
+ || enrollment.contract !== "hellolynk-device-enrollment-v1"
214
+ || !Number.isSafeInteger(enrollment.revision) || enrollment.revision < 0
215
+ || (enrollment.value !== null && (typeof enrollment.value !== "object" || Array.isArray(enrollment.value)))) {
216
+ fail("device_enrollment_store_invalid");
217
+ }
218
+ if (!equal(document, { host:emptyHost(enrollment) })) fail("host_unclaimed_authorization_present");
219
+ });
220
+ }
221
+
222
+ export function invokeDarwinHost(q) {
223
+ const readOnly = q.verifyOnly || q.command.startsWith("Verify");
224
+ return withDarwinVault({ root:q.root, helperPath:q.helperPath, readOnly, signal:q.signal }, document => {
225
+ const s = document.host || emptyHost();
226
+ if (!readOnly) document.host = s;
227
+ const input = q.stdin ? JSON.parse(q.stdin) : null;
228
+ const old = s.caps.find(c => c.capabilityId === q.capabilityId);
229
+ switch (q.command) {
230
+ case "Status": return {contract:"hellolynk-host-protected-store-v1",protected:true};
231
+ case "LoadDeviceEnrollment": return s.enrollment;
232
+ case "SaveDeviceEnrollment":
233
+ if (s.enrollment.revision !== input.expectedRevision) fail("device_enrollment_revision_conflict");
234
+ s.enrollment.value = input.value;
235
+ s.enrollment.revision++;
236
+ return s.enrollment;
237
+ case "Issue": addCapability(s,input); return {record:input};
238
+ case "Load":
239
+ if (old?.state !== "active") fail("host_capability_inactive");
240
+ return {record:old};
241
+ case "Revoke": return revokeCapability(s,old);
242
+ case "Rotate":
243
+ if (old?.state !== "active") fail("host_capability_inactive");
244
+ addCapability(s,input);
245
+ revokeCapability(s,old);
246
+ return {record:input, replacedCapabilityId:q.capabilityId};
247
+ case "DisconnectLocalClient":
248
+ for (const cap of s.caps.filter(c => c.adapterId === input.client && c.state === "active")) revokeCapability(s,cap);
249
+ if (input.client === "codex") s.codex = null;
250
+ return {disconnected:true,client:input.client};
251
+ }
252
+ if (q.command.endsWith("CodexNeutralBinding")) return codexBinding(s,q.command,input,q.capabilityId);
253
+ if (q.command.includes("Transport")) return transport(s,q,input);
254
+ fail("host_protected_store_command_unsupported");
255
+ });
256
+ }
@@ -0,0 +1,153 @@
1
+ // macOS-only storage. Keychain holds the wrapping key. The SQLite rollback
2
+ // journal and database contain a single authenticated, root-bound ciphertext.
3
+ import { spawn } from "node:child_process";
4
+ import { createCipheriv, createDecipheriv, randomBytes } from "node:crypto";
5
+ import { closeSync, constants, lstatSync, mkdirSync, openSync } from "node:fs";
6
+ import { dirname, join, resolve } from "node:path";
7
+ import { fileURLToPath } from "node:url";
8
+ import { homedir } from "node:os";
9
+
10
+ export const DARWIN_HELPER = fileURLToPath(new URL("../platform/darwin/hellolynk-keychain-helper.bin", import.meta.url));
11
+ export const DARWIN_VAULT_FILE = "protected-v1.sqlite";
12
+ // Neither the wrapping-key process nor the approval UI needs a caller's
13
+ // shell, language runtime, dynamic-loader overrides or credential environment.
14
+ export function darwinHelperEnvironment() {
15
+ return { HOME: homedir(), PATH: "/usr/bin:/bin", LANG: "en_US.UTF-8" };
16
+ }
17
+ export function vaultError(code) { return Object.assign(new Error(code), { code }); }
18
+
19
+ // Validate before creating any missing components (including in a supplied
20
+ // storeRoot). Never follow links or create children below a linked ancestor.
21
+ export function checkDarwinPath(target, { directory = false, privateMode = false, create = false, missing = false } = {}) {
22
+ if (target === "/" || target !== resolve(target)) throw vaultError("macos_vault_path_invalid");
23
+ const parts = target.slice(1).split("/");
24
+ let current = "/";
25
+ for (const [index, name] of parts.entries()) {
26
+ current = join(current, name);
27
+ const dir = index < parts.length - 1 || directory;
28
+ let st = lstatSync(current, { throwIfNoEntry: false });
29
+ if (!st && create && dir) {
30
+ try { mkdirSync(current, { mode: 0o700 }); }
31
+ catch (error) { if (error.code !== "EEXIST") throw error; }
32
+ st = lstatSync(current);
33
+ }
34
+ if (!st && missing) return null;
35
+ if (!st || st.isSymbolicLink() || (dir ? !st.isDirectory() : !st.isFile())) throw vaultError("macos_vault_path_invalid");
36
+ if (index < parts.length - 1 && ((st.uid !== 0 && st.uid !== process.getuid()) ||
37
+ ((st.mode & 0o022) && !(st.uid === 0 && (st.mode & 0o1000))))) {
38
+ throw vaultError("macos_vault_ancestor_invalid");
39
+ }
40
+ if (index === parts.length - 1) {
41
+ if (privateMode && (st.uid !== process.getuid() || (st.mode & 0o077) || (!dir && st.nlink !== 1))) {
42
+ throw vaultError("macos_vault_permissions_invalid");
43
+ }
44
+ return st;
45
+ }
46
+ }
47
+ }
48
+
49
+ export async function invokeDarwinHelper(helper, args, signal) {
50
+ const st = checkDarwinPath(helper);
51
+ if ((st.mode & 0o022) || (st.uid !== 0 && st.uid !== process.getuid())) throw vaultError("macos_vault_helper_invalid");
52
+ signal?.throwIfAborted();
53
+ return new Promise((ok, reject) => {
54
+ const child = spawn(helper, args, { stdio: ["ignore", "ignore", "pipe", "pipe"],
55
+ env: darwinHelperEnvironment(), signal });
56
+ const chunks = [];
57
+ let size = 0, stderr = "";
58
+ const timer = setTimeout(() => child.kill("SIGKILL"), 30_000);
59
+ child.stdio[3].on("data", chunk => {
60
+ size += chunk.length;
61
+ if (size <= 32) chunks.push(chunk);
62
+ else child.kill("SIGKILL");
63
+ });
64
+ child.stderr.on("data", chunk => {
65
+ stderr += chunk;
66
+ if (stderr.length > 256) child.kill("SIGKILL");
67
+ });
68
+ child.on("error", error => { clearTimeout(timer); reject(error); });
69
+ child.on("close", code => {
70
+ clearTimeout(timer);
71
+ const data = Buffer.concat(chunks);
72
+ chunks.forEach(chunk => chunk.fill(0));
73
+ if (code === 0 && data.length === (args[0] === "key" ? 32 : 0)) return ok(data);
74
+ data.fill(0);
75
+ reject(vaultError(/^macos_[a-z0-9_]{1,100}$/.test(stderr.trim()) ? stderr.trim() : "macos_vault_helper_failed"));
76
+ });
77
+ });
78
+ }
79
+
80
+ function openDocument(key, aad, bytes) {
81
+ try {
82
+ const blob = Buffer.from(bytes);
83
+ const cipher = createDecipheriv("aes-256-gcm", key, blob.subarray(0, 12));
84
+ cipher.setAAD(aad);
85
+ cipher.setAuthTag(blob.subarray(12, 28));
86
+ return Buffer.concat([cipher.update(blob.subarray(28)), cipher.final()]);
87
+ } catch { throw vaultError("macos_vault_authentication_failed"); }
88
+ }
89
+
90
+ function sealDocument(key, aad, plain) {
91
+ const nonce = randomBytes(12);
92
+ const cipher = createCipheriv("aes-256-gcm", key, nonce);
93
+ cipher.setAAD(aad);
94
+ const body = Buffer.concat([cipher.update(plain), cipher.final()]);
95
+ return Buffer.concat([nonce, cipher.getAuthTag(), body]);
96
+ }
97
+
98
+ // The callback is synchronous. No event-loop work or network I/O is performed
99
+ // while SQLite owns the write lock. Concurrent first writers share SecItemAdd's
100
+ // winning key and re-read the committed document after BEGIN IMMEDIATE.
101
+ export async function withDarwinVault({ root, helperPath = DARWIN_HELPER, readOnly = false, allowMissing = false, signal }, operation) {
102
+ const { DatabaseSync } = await import("node:sqlite");
103
+ const directory = checkDarwinPath(root, { directory: true, privateMode: true, create: !readOnly, missing: readOnly && allowMissing });
104
+ const path = join(root, DARWIN_VAULT_FILE);
105
+ let stat = directory && checkDarwinPath(path, { privateMode: true, missing: true });
106
+ if (!stat) {
107
+ if (readOnly) {
108
+ if (allowMissing) return operation({});
109
+ throw vaultError("host_protected_store_path_invalid");
110
+ }
111
+ try { closeSync(openSync(path, constants.O_CREAT | constants.O_EXCL | constants.O_WRONLY | constants.O_NOFOLLOW, 0o600)); }
112
+ catch (error) { if (error.code !== "EEXIST") throw error; }
113
+ stat = checkDarwinPath(path, { privateMode: true });
114
+ }
115
+ const newFile = stat.size === 0;
116
+ const key = await invokeDarwinHelper(helperPath, ["key", root, !readOnly && newFile ? "create" : "load"], signal);
117
+ let db, plain, encoded;
118
+ try {
119
+ signal?.throwIfAborted();
120
+ checkDarwinPath(path, { privateMode: true });
121
+ checkDarwinPath(path + "-journal", { privateMode: true, missing: true });
122
+ for (const suffix of ["-wal", "-shm"]) {
123
+ if (lstatSync(path + suffix, { throwIfNoEntry: false })) throw vaultError("macos_vault_journal_invalid");
124
+ }
125
+ db = new DatabaseSync(path, { readOnly, allowExtension: false });
126
+ db.exec("PRAGMA trusted_schema=OFF; PRAGMA busy_timeout=10000");
127
+ if (db.prepare("PRAGMA journal_mode").get().journal_mode !== "delete") throw vaultError("macos_vault_journal_invalid");
128
+ db.exec(readOnly ? "BEGIN" : "BEGIN IMMEDIATE");
129
+ const schema = db.prepare("SELECT sql FROM sqlite_schema WHERE name = 'vault' AND type = 'table'").get();
130
+ if (!schema) {
131
+ if (!newFile || readOnly) throw vaultError("macos_vault_format_invalid");
132
+ db.exec("CREATE TABLE vault (id INTEGER PRIMARY KEY CHECK(id=1), sealed BLOB NOT NULL)");
133
+ }
134
+ const row = db.prepare("SELECT sealed FROM vault WHERE id=1").get();
135
+ if (schema && !row) throw vaultError("macos_vault_format_invalid");
136
+ const aad = Buffer.from("hellolynk-macos-vault-v1\0" + root);
137
+ plain = row ? openDocument(key, aad, row.sealed) : Buffer.from("{}");
138
+ const state = JSON.parse(plain);
139
+ const result = operation(state);
140
+ encoded = Buffer.from(JSON.stringify(state));
141
+ if (!row || !plain.equals(encoded)) {
142
+ if (readOnly) throw vaultError("host_protected_store_read_only");
143
+ db.prepare("INSERT OR REPLACE INTO vault(id,sealed) VALUES(1,?)").run(sealDocument(key, aad, encoded));
144
+ }
145
+ db.exec("COMMIT");
146
+ return result;
147
+ } finally {
148
+ key.fill(0);
149
+ plain?.fill(0);
150
+ encoded?.fill(0);
151
+ db?.close(); // closes or rolls back a transaction whose callback failed
152
+ }
153
+ }
@@ -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;