@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.
- package/README.md +17 -17
- package/bin/hellolynk-client-setup.mjs +70 -10
- package/hellolynk-client-pack-manifest.json +101 -61
- package/hellolynk-client-pack-pin.json +1 -1
- package/package.json +8 -4
- package/plugins/hellolynk-codex/.codex-plugin/plugin.json +1 -1
- package/plugins/hellolynk-codex/.mcp.json +20 -19
- package/plugins/hellolynk-codex/package.json +1 -1
- package/plugins/hellolynk-codex/runtime/bootstrap/core/hellolynk-client-account.mjs +1 -1
- package/plugins/hellolynk-codex/runtime/bootstrap/core/hellolynk-darwin-host.mjs +269 -0
- package/plugins/hellolynk-codex/runtime/bootstrap/core/hellolynk-darwin-vault.mjs +153 -0
- package/plugins/hellolynk-codex/runtime/bootstrap/core/hellolynk-host-protected-store.mjs +20 -8
- package/plugins/hellolynk-codex/runtime/bootstrap/core/hellolynk-neutral-host-installation-store.mjs +1 -1
- package/plugins/hellolynk-codex/runtime/bootstrap/core/hellolynk-platform-credential-store.mjs +68 -14
- package/plugins/hellolynk-codex/runtime/bootstrap/platform/darwin/hellolynk-keychain-helper.bin +0 -0
- package/plugins/hellolynk-codex/runtime/bootstrap/platform/darwin/hellolynk-keychain-helper.swift +149 -0
- package/plugins/hellolynk-codex/runtime/connect-activate.mjs +5 -1
- package/plugins/hellolynk-codex/runtime/core/hellolynk-client-account.mjs +1 -1
- package/plugins/hellolynk-codex/runtime/core/hellolynk-client-update-entry.mjs +7 -3
- package/plugins/hellolynk-codex/runtime/core/hellolynk-client-update-runtime.mjs +62 -33
- package/plugins/hellolynk-codex/runtime/core/hellolynk-client-update.mjs +62 -18
- package/plugins/hellolynk-codex/runtime/core/hellolynk-darwin-host.mjs +269 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-darwin-vault.mjs +153 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-host-protected-store.mjs +20 -8
- package/plugins/hellolynk-codex/runtime/core/hellolynk-human-confirmation-ledger.mjs +53 -6
- package/plugins/hellolynk-codex/runtime/core/hellolynk-local-data-protection.mjs +22 -7
- package/plugins/hellolynk-codex/runtime/core/hellolynk-native-human-confirmation-broker.mjs +7 -5
- package/plugins/hellolynk-codex/runtime/core/hellolynk-neutral-host-installation-store.mjs +1 -1
- package/plugins/hellolynk-codex/runtime/core/hellolynk-neutral-host-launcher.mjs +9 -3
- package/plugins/hellolynk-codex/runtime/core/hellolynk-neutral-host-process.mjs +1 -1
- package/plugins/hellolynk-codex/runtime/core/hellolynk-platform-credential-store.mjs +68 -14
- package/plugins/hellolynk-codex/runtime/core/hellolynk-release-retention.mjs +101 -2
- package/plugins/hellolynk-codex/runtime/core/hellolynk-workbench-connection.mjs +5 -3
- package/plugins/hellolynk-codex/runtime/neutral-host-release-entry.mjs +1 -1
- package/plugins/hellolynk-codex/runtime/neutral-host-release-loader.mjs +1 -1
- package/plugins/hellolynk-codex/runtime/neutral-host-release-manifest.json +56 -24
- package/plugins/hellolynk-codex/runtime/neutral-host-release-pin.json +1 -1
- package/plugins/hellolynk-codex/runtime/platform/darwin/hellolynk-keychain-helper.bin +0 -0
- package/plugins/hellolynk-codex/runtime/platform/darwin/hellolynk-keychain-helper.swift +149 -0
- package/plugins/hellolynk-codex/runtime/platform/windows/credential-store.ps1 +46 -10
- package/plugins/hellolynk-codex/server/codex-neutral-production.mjs +14 -8
- 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(
|
|
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 (
|
|
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
|
|
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
|
-
|
|
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 || (
|
|
32
|
-
command, value
|
|
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 (
|
|
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 || (
|
|
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,
|
|
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
|
-
|
|
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
|
|
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
|
|
596
|
+
await runNativeDialogChild(serialized, context);
|
|
596
597
|
},
|
|
597
598
|
});
|
|
598
599
|
}
|
|
599
600
|
|
|
600
|
-
async function
|
|
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
|
|
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
|
-
|
|
67
|
-
|
|
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 =
|
|
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
|
|
464
|
-
homedir(), "Library", "Application Support", "HelloLynk"
|
|
465
|
-
);
|
|
466
|
-
const
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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(
|
|
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
|
|
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)), {
|
|
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(
|
|
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),
|