@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.
- package/README.md +17 -17
- package/bin/hellolynk-client-setup.mjs +79 -11
- package/hellolynk-client-pack-manifest.json +111 -71
- 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-client-profile.mjs +25 -2
- package/plugins/hellolynk-codex/runtime/bootstrap/core/hellolynk-darwin-host.mjs +256 -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 +3 -7
- 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-profile.mjs +25 -2
- package/plugins/hellolynk-codex/runtime/core/hellolynk-client-update-entry.mjs +7 -3
- package/plugins/hellolynk-codex/runtime/core/hellolynk-client-update-runtime.mjs +24 -8
- package/plugins/hellolynk-codex/runtime/core/hellolynk-client-update.mjs +23 -9
- package/plugins/hellolynk-codex/runtime/core/hellolynk-darwin-host.mjs +256 -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-addressing-tools.cjs +1 -1
- 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 +3 -7
- 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 +116 -19
- 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 +59 -27
- 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/server/codex-neutral-production.mjs +17 -9
- package/plugins/hellolynk-codex/server/mcp-server.mjs +1193 -1193
- package/plugins/hellolynk-codex/server/native-conversation-directory.mjs +33 -33
- package/plugins/hellolynk-codex/server/workbench-runtime.mjs +42 -42
- package/tools/hellolynk-activate-neutral-host-clients.mjs +5 -1
|
@@ -18,7 +18,7 @@ const conversationGuidance = "HelloLynk 是本人授权的 AI 通讯。接着已
|
|
|
18
18
|
// What every client shows for the shared tools; nativeTool applies it.
|
|
19
19
|
const toolText = Object.freeze({
|
|
20
20
|
hellolynk_reply_authorized_message: { title: "回复来信", description: "接着当前对话的一封来信发送;不接手其他对话。" },
|
|
21
|
-
hellolynk_send_authorized_message: { title: "发起通信", description: "向本人授权的联系人发起新通信。contact 填对方名字;本人点名了对话时,targetConversationTitle
|
|
21
|
+
hellolynk_send_authorized_message: { title: "发起通信", description: "向本人授权的联系人发起新通信。contact 填对方名字;本人点名了对话时,targetConversationTitle 填完整标题。未指定对话的新通信默认送到对方的 HelloLynk 独立会话;要继续已有来往,用 hellolynk_reply_authorized_message 回复原信。" },
|
|
22
22
|
hellolynk_list_authorized_contacts: { title: "查找联系人", description: queryDescription },
|
|
23
23
|
hellolynk_claim_message: { title: "读取来信", description: "按自动提醒里的 batchId 一次读取整批来信(合计不超过 2 万字);手动读单条传 localMessageId 和 conversationKey。" },
|
|
24
24
|
hellolynk_list_pending_messages: { title: "待处理来信", description: "列出当前对话等待 AI 处理的来信,不含正文。" },
|
|
@@ -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;
|
|
@@ -12,10 +12,9 @@ import {
|
|
|
12
12
|
} from "node:fs/promises";
|
|
13
13
|
import { basename, dirname, isAbsolute, join, parse, resolve } from "node:path";
|
|
14
14
|
import { spawn } from "node:child_process";
|
|
15
|
-
import { homedir } from "node:os";
|
|
16
15
|
import { performance } from "node:perf_hooks";
|
|
17
16
|
import { withLocalProcessLock } from "./hellolynk-local-process-lock.mjs";
|
|
18
|
-
import { readClientProfile } from "./hellolynk-client-profile.mjs";
|
|
17
|
+
import { defaultHelloLynkClientRoot, readClientProfile } from "./hellolynk-client-profile.mjs";
|
|
19
18
|
import { assertIdentifier } from "./hellolynk-host-auth.mjs";
|
|
20
19
|
import runtimeSafety from "./hellolynk-runtime-safety.cjs";
|
|
21
20
|
|
|
@@ -46,10 +45,7 @@ export function defaultNeutralHostStoreRoot(platform = process.platform, env = p
|
|
|
46
45
|
}
|
|
47
46
|
return root;
|
|
48
47
|
}
|
|
49
|
-
|
|
50
|
-
if (platform === "win32") return resolve(String(env.LOCALAPPDATA || join(home, "AppData", "Local")), "HelloLynk", "host");
|
|
51
|
-
if (platform === "darwin") return join(home, "Library", "Application Support", "HelloLynk", "host");
|
|
52
|
-
return resolve(String(env.XDG_DATA_HOME || join(home, ".local", "share")), "hellolynk", "host");
|
|
48
|
+
return join(defaultHelloLynkClientRoot(platform, env), "host");
|
|
53
49
|
}
|
|
54
50
|
|
|
55
51
|
export function createNeutralHostInstallationStore(options = {}) {
|
|
@@ -344,7 +340,7 @@ async function assertReadOnlyPath(path, directory, verifyPath) {
|
|
|
344
340
|
if (metadata.isSymbolicLink() || (directory ? !metadata.isDirectory() : !metadata.isFile())) {
|
|
345
341
|
throw codeError(directory ? "host_store_root_invalid" : "host_registry_path_invalid");
|
|
346
342
|
}
|
|
347
|
-
if (process.platform === "win32") {
|
|
343
|
+
if (process.platform === "win32" || (process.platform === "darwin" && verifyPath)) {
|
|
348
344
|
await verifyPath({ path, directory });
|
|
349
345
|
}
|
|
350
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 = 15;
|
|
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);
|
|
@@ -15,12 +16,29 @@ const contract = 'hellolynk-release-retention-v1';
|
|
|
15
16
|
// receipt: an older worker that cleans up after an update deletes nothing.
|
|
16
17
|
const ENTRY = { 'claude-code': ['claude-code', 'hellolynk-claude-code.mjs'] };
|
|
17
18
|
const contractWithEntry = 'hellolynk-release-retention-v2';
|
|
19
|
+
// B255 workers understand v2 but do not read Codex process records.
|
|
20
|
+
const codexContract = 'hellolynk-release-retention-v3';
|
|
18
21
|
const PROCESS_CONTRACT = 'hellolynk-release-process-v1';
|
|
19
22
|
const START_TOLERANCE_MS = 15000;
|
|
20
23
|
export const releaseProcessDirectory = (releasesRoot, client) => path.join(releasesRoot, `.${client}-processes`);
|
|
21
24
|
const failure = code => Object.assign(new Error(code), { code });
|
|
22
25
|
const equal = (a, b) => process.platform === 'win32' ? a.toLowerCase() === b.toLowerCase() : a === b;
|
|
23
26
|
|
|
27
|
+
// A cache MCP keeps an installed prepare function in memory. Record its most
|
|
28
|
+
// recent successful binding, even when its command line names only the cache.
|
|
29
|
+
// A checkout/uninstalled fixture has no immutable release to retain.
|
|
30
|
+
export async function recordCodexReleaseProcess(packageRoot) {
|
|
31
|
+
if (!['win32', 'darwin'].includes(process.platform) || !path.isAbsolute(packageRoot || '')
|
|
32
|
+
|| path.basename(path.dirname(packageRoot)) !== 'releases' || !/^codex-[a-f0-9]{64}$/.test(path.basename(packageRoot))) return;
|
|
33
|
+
try {
|
|
34
|
+
const root = await rootPath(path.dirname(packageRoot));
|
|
35
|
+
const folder = releaseProcessDirectory(root, 'codex');
|
|
36
|
+
await fs.mkdir(folder, { recursive: true });
|
|
37
|
+
await writeReceipt(path.join(folder, `${process.pid}.json`), { contract: PROCESS_CONTRACT, pid: process.pid,
|
|
38
|
+
startedAt: Math.round(performance.timeOrigin), release: path.basename(packageRoot) });
|
|
39
|
+
} catch { /* A missing record defers cleanup; it must not stop communication. */ }
|
|
40
|
+
}
|
|
41
|
+
|
|
24
42
|
async function directory(target) {
|
|
25
43
|
const stat = await fs.lstat(target);
|
|
26
44
|
if (!stat.isDirectory() || stat.isSymbolicLink()) throw failure('release_retention_path_invalid');
|
|
@@ -53,6 +71,10 @@ async function regularTree(target) {
|
|
|
53
71
|
// older Host may still import files after installation; deletion waits for the
|
|
54
72
|
// next successful install instead of stopping any user process.
|
|
55
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
|
+
}
|
|
56
78
|
if (process.platform !== 'win32') throw failure('release_retention_process_check_unavailable');
|
|
57
79
|
const root = await rootPath(releasesRoot);
|
|
58
80
|
const literal = `${root}${path.sep}`.replaceAll("'", "''");
|
|
@@ -67,20 +89,29 @@ export async function runningReleaseNames(releasesRoot) {
|
|
|
67
89
|
return names;
|
|
68
90
|
}
|
|
69
91
|
|
|
70
|
-
// The given pids
|
|
71
|
-
//
|
|
92
|
+
// The given pids, this installation's stable entry, and Codex cache children:
|
|
93
|
+
// pid, start time and whether it runs the entry. Only processes
|
|
72
94
|
// with a positive pid are listed, and command lines never leave PowerShell.
|
|
73
95
|
export async function entryProcessList(client, releasesRoot, pids = []) {
|
|
74
|
-
if (!ENTRY[client]) return [];
|
|
75
|
-
if (process.platform !== 'win32') throw failure('release_retention_process_check_unavailable');
|
|
96
|
+
if (!ENTRY[client] && client !== 'codex') return [];
|
|
76
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' && client === 'codex') {
|
|
99
|
+
await rootPath(releasesRoot);
|
|
100
|
+
return parseDarwinCodexEntryProcesses(await darwinPs('pid=,ppid=,lstart=,command='), pids);
|
|
101
|
+
}
|
|
102
|
+
if (process.platform !== 'win32') throw failure('release_retention_process_check_unavailable');
|
|
77
103
|
const root = await rootPath(releasesRoot);
|
|
78
104
|
// This installation's entry only, in its given and canonical spellings:
|
|
79
105
|
// any case, either slash, quoted or not.
|
|
80
106
|
const literal = value => value.split(/[\\/]+/).map(part => part.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')).join('[\\\\/]+');
|
|
81
|
-
const entries = [...new Set([root, path.resolve(releasesRoot)])].map(value => literal(path.join(path.dirname(value), ...ENTRY[client])));
|
|
82
|
-
const pattern =
|
|
83
|
-
|
|
107
|
+
const entries = ENTRY[client] ? [...new Set([root, path.resolve(releasesRoot)])].map(value => literal(path.join(path.dirname(value), ...ENTRY[client]))) : [];
|
|
108
|
+
const pattern = (client === 'codex' ? '(?:^|["\\s\\\\/])(?:\\.[\\\\/])?server[\\\\/]mcp-proxy\\.mjs(?=["\\s]|$)'
|
|
109
|
+
: `(?<=^|["\\s])(?:${entries.join('|')})(?=["\\s]|$)`).replaceAll("'", "''");
|
|
110
|
+
// Relative cache entries do not reveal their installation. An older Codex
|
|
111
|
+
// child without a record conservatively delays this client's cleanup until
|
|
112
|
+
// it reconnects through a recording release or exits. Other Node apps do not.
|
|
113
|
+
const parentCheck = client === 'codex' ? "$_.Name -ieq 'node.exe' -and ($codexParents -contains $_.ParentProcessId) -and " : '';
|
|
114
|
+
const script = `$ErrorActionPreference='Stop'; $wanted=@(${pids.join(',')}); $epoch=[datetime]::new(1970,1,1,0,0,0,[DateTimeKind]::Utc); $all=@(Get-CimInstance Win32_Process); $codexParents=@($all | Where-Object { $_.Name -ieq 'Codex.exe' } | ForEach-Object { $_.ProcessId }); $list=@($all | Where-Object { $_.ProcessId -gt 0 } | ForEach-Object { $entry=[bool](${parentCheck}$_.CommandLine -and [regex]::IsMatch($_.CommandLine,'${pattern}','IgnoreCase')); if ($entry -or ($wanted -contains [int64]$_.ProcessId)) { [pscustomobject]@{ pid=[int64]$_.ProcessId; startedAt=$(if ($_.CreationDate) { [int64](($_.CreationDate.ToUniversalTime() - $epoch).TotalMilliseconds) } else { $null }); entry=$entry } } }); ConvertTo-Json -Compress -InputObject $list`;
|
|
84
115
|
const shell = path.join(process.env.SystemRoot || process.env.SYSTEMROOT || 'C:\\Windows', 'System32/WindowsPowerShell/v1.0/powershell.exe');
|
|
85
116
|
const { stdout } = await execute(shell, ['-NoLogo', '-NoProfile', '-NonInteractive', '-Command', script],
|
|
86
117
|
{ windowsHide: true, timeout: 15000, maxBuffer: 1024 * 1024 });
|
|
@@ -90,12 +121,51 @@ export async function entryProcessList(client, releasesRoot, pids = []) {
|
|
|
90
121
|
return list;
|
|
91
122
|
}
|
|
92
123
|
|
|
124
|
+
async function darwinPs(fields) {
|
|
125
|
+
const { stdout } = await execute('/bin/ps', ['-ww', '-axo', fields], {
|
|
126
|
+
env: { LC_ALL: 'C', TZ: 'UTC0' }, timeout: 15000, maxBuffer: 4 * 1024 * 1024,
|
|
127
|
+
});
|
|
128
|
+
return stdout;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Keep command lines inside the process probe. Only a PID, its start time and
|
|
132
|
+
// whether it might still be serving the Codex cache reach the retention logic.
|
|
133
|
+
export function parseDarwinCodexEntryProcesses(stdout, pids = []) {
|
|
134
|
+
const processes = new Map();
|
|
135
|
+
for (const line of stdout.trim().split('\n')) {
|
|
136
|
+
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);
|
|
137
|
+
if (!fields) throw failure('release_retention_process_check_invalid');
|
|
138
|
+
const [, pid, parent, started, command] = fields;
|
|
139
|
+
const startedAt = Date.parse(`${started} UTC`);
|
|
140
|
+
processes.set(Number(pid), { pid: Number(pid), parent: Number(parent), startedAt: Number.isFinite(startedAt) ? startedAt : null, command });
|
|
141
|
+
}
|
|
142
|
+
const wanted = new Set(pids);
|
|
143
|
+
const proxy = /(?:^|["\s/])(?:\.\/)?server\/mcp-proxy\.mjs(?=["\s]|$)/u;
|
|
144
|
+
const codexApp = /(?:^|["/])(?:Codex|ChatGPT)\.app\/Contents\//iu;
|
|
145
|
+
const codexCli = /^(?:"?[^"\s]*\/)?codex(?=["\s]|$)/iu;
|
|
146
|
+
const result = [];
|
|
147
|
+
for (const { pid, parent, startedAt, command } of processes.values()) {
|
|
148
|
+
let entry = false;
|
|
149
|
+
if (proxy.test(command) && /(?:^|\/)node(?=["\s]|$)/u.test(command)) {
|
|
150
|
+
entry = /\/cua_node\/bin\/node(?=["\s]|$)/u.test(command)
|
|
151
|
+
|| /\/hellolynk-codex\/server\/mcp-proxy\.mjs(?=["\s]|$)/u.test(command);
|
|
152
|
+
const seen = new Set([pid]);
|
|
153
|
+
for (let ancestor = processes.get(parent); !entry && ancestor && !seen.has(ancestor.pid); ancestor = processes.get(ancestor.parent)) {
|
|
154
|
+
entry = codexApp.test(ancestor.command) || codexCli.test(ancestor.command);
|
|
155
|
+
seen.add(ancestor.pid);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
if (entry || wanted.has(pid)) result.push({ pid, startedAt, entry });
|
|
159
|
+
}
|
|
160
|
+
return result;
|
|
161
|
+
}
|
|
162
|
+
|
|
93
163
|
// Releases named by live entry records, and whether a process started through
|
|
94
164
|
// the stable entry has no matching record (one from before records existed).
|
|
95
165
|
// Records of exited or reused pids are removed, so a release can go at the
|
|
96
166
|
// next install once its last process is gone.
|
|
97
167
|
async function entryReleases(releasesRoot, root, client, pattern, dependencies) {
|
|
98
|
-
if (!ENTRY[client]) return { names: [], unregistered: false };
|
|
168
|
+
if (!ENTRY[client] && client !== 'codex') return { names: [], unregistered: false };
|
|
99
169
|
const folder = releaseProcessDirectory(root, client);
|
|
100
170
|
let items = [];
|
|
101
171
|
try {
|
|
@@ -127,6 +197,24 @@ async function entryReleases(releasesRoot, root, client, pattern, dependencies)
|
|
|
127
197
|
// A stopped on-demand task is still a configuration reference. Keeping only
|
|
128
198
|
// live processes can delete its executable before the next client startup.
|
|
129
199
|
export async function configuredReleaseNames(releasesRoot) {
|
|
200
|
+
if (process.platform === 'darwin') {
|
|
201
|
+
const pattern = darwinReleasePattern(await rootPath(releasesRoot), releasesRoot);
|
|
202
|
+
const names = new Set();
|
|
203
|
+
for (const directory of [path.join(homedir(), 'Library', 'LaunchAgents'), '/Library/LaunchAgents', '/Library/LaunchDaemons']) {
|
|
204
|
+
let entries;
|
|
205
|
+
try { entries = await fs.readdir(directory); }
|
|
206
|
+
catch (error) { if (error.code === 'ENOENT') continue; throw error; }
|
|
207
|
+
for (const entry of entries.filter(name => /hellolynk/i.test(name) && name.endsWith('.plist'))) {
|
|
208
|
+
const { stdout } = await execute('/usr/bin/plutil', ['-convert', 'json', '-o', '-', path.join(directory, entry)],
|
|
209
|
+
{ timeout: 15000, maxBuffer: 256 * 1024 });
|
|
210
|
+
const { Program, WorkingDirectory, ProgramArguments } = JSON.parse(stdout);
|
|
211
|
+
for (const value of [Program, WorkingDirectory, ...(ProgramArguments || [])]) {
|
|
212
|
+
if (typeof value === 'string') for (const name of darwinReleaseNames(value, pattern)) names.add(name);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
return [...names].sort();
|
|
217
|
+
}
|
|
130
218
|
if (process.platform !== 'win32') throw failure('release_retention_configuration_check_unavailable');
|
|
131
219
|
const root = await rootPath(releasesRoot);
|
|
132
220
|
const prefixes = [...new Set([root, path.resolve(releasesRoot)])].map(value => (value + path.sep).replace(/[.*+?^${}()|[\]\\]/g, '\\$&'));
|
|
@@ -142,6 +230,16 @@ export async function configuredReleaseNames(releasesRoot) {
|
|
|
142
230
|
return names;
|
|
143
231
|
}
|
|
144
232
|
|
|
233
|
+
function darwinReleasePattern(root, logicalRoot) {
|
|
234
|
+
const prefixes = [...new Set([root, path.resolve(logicalRoot)])]
|
|
235
|
+
.map(value => (value + path.sep).replace(/[.*+?^${}()|[\]\\]/g, '\\$&'));
|
|
236
|
+
return new RegExp('(?:' + prefixes.join('|') + ')((?:' + clients.join('|') + ')-[a-f0-9]{64})(?=[/"\\s]|$)', 'g');
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function darwinReleaseNames(text, pattern) {
|
|
240
|
+
return [...new Set([...text.matchAll(pattern)].map(match => match[1]))].sort();
|
|
241
|
+
}
|
|
242
|
+
|
|
145
243
|
// Call only after the caller's complete adoption transaction succeeds, while
|
|
146
244
|
// holding its install/account lock. Never call from candidate staging or a
|
|
147
245
|
// rollback. The receipt records adoption order, not directory timestamps.
|
|
@@ -179,21 +277,20 @@ export async function pruneInstalledReleases({ releasesRoot, client, currentRele
|
|
|
179
277
|
return result;
|
|
180
278
|
}
|
|
181
279
|
|
|
182
|
-
//
|
|
183
|
-
//
|
|
184
|
-
//
|
|
185
|
-
//
|
|
186
|
-
// v2, keeping the adoption it records, with whether it wrote. Throws when that
|
|
280
|
+
// Call before switching the binding/entry. An earlier updater cleans up after
|
|
281
|
+
// setup with its own code. Claude needs v2 to stop pre-B255 workers; Codex needs
|
|
282
|
+
// v3 to stop B255 workers that know only Claude records. Preserve the recorded
|
|
283
|
+
// adoption order and resolve with whether the receipt changed. Throw when that
|
|
187
284
|
// cannot be confirmed, even for a cause that passes before the worker reads the
|
|
188
285
|
// receipt (a file another process holds open): the caller then switches nothing.
|
|
189
286
|
// currentRelease, the release the install starts now, is recorded only when
|
|
190
287
|
// there is no receipt yet.
|
|
191
288
|
export async function protectRetentionReceipt({ releasesRoot, client, currentRelease }) {
|
|
192
|
-
if (!ENTRY[client]) throw failure('release_retention_client_invalid');
|
|
289
|
+
if (!ENTRY[client] && client !== 'codex') throw failure('release_retention_client_invalid');
|
|
193
290
|
const root = await rootPath(releasesRoot), pattern = new RegExp(`^${client}-[a-f0-9]{64}$`);
|
|
194
291
|
const receiptPath = path.join(root, `.${client}-retention.json`);
|
|
195
292
|
const old = await readReceipt(receiptPath, client, pattern);
|
|
196
|
-
if (old?.contract ===
|
|
293
|
+
if (old?.contract === receiptContract(client)) return false;
|
|
197
294
|
let current = old?.current;
|
|
198
295
|
if (!current) {
|
|
199
296
|
if (typeof currentRelease !== 'string' || !path.isAbsolute(currentRelease) || !pattern.test(path.basename(currentRelease))
|
|
@@ -201,11 +298,11 @@ export async function protectRetentionReceipt({ releasesRoot, client, currentRel
|
|
|
201
298
|
current = path.basename(currentRelease);
|
|
202
299
|
await directory(path.join(root, current));
|
|
203
300
|
}
|
|
204
|
-
await writeReceipt(receiptPath, { contract:
|
|
301
|
+
await writeReceipt(receiptPath, { contract: receiptContract(client), client, current, previous: old ? old.previous : null });
|
|
205
302
|
return true;
|
|
206
303
|
}
|
|
207
304
|
|
|
208
|
-
const receiptContract = client => ENTRY[client] ? contractWithEntry : contract;
|
|
305
|
+
const receiptContract = client => client === 'codex' ? codexContract : ENTRY[client] ? contractWithEntry : contract;
|
|
209
306
|
|
|
210
307
|
async function readReceipt(receiptPath, client, pattern) {
|
|
211
308
|
let old = null;
|
|
@@ -213,7 +310,7 @@ async function readReceipt(receiptPath, client, pattern) {
|
|
|
213
310
|
const stat = await fs.lstat(receiptPath);
|
|
214
311
|
if (!stat.isFile() || stat.isSymbolicLink() || stat.nlink !== 1 || stat.size > 4096) throw failure('release_retention_receipt_invalid');
|
|
215
312
|
old = JSON.parse(await fs.readFile(receiptPath, 'utf8'));
|
|
216
|
-
if (![contract, contractWithEntry].includes(old?.contract) || old.client !== client || !pattern.test(old.current)
|
|
313
|
+
if (![contract, contractWithEntry, ...(client === 'codex' ? [codexContract] : [])].includes(old?.contract) || old.client !== client || !pattern.test(old.current)
|
|
217
314
|
|| old.previous !== null && !pattern.test(old.previous) || old.current === old.previous) throw failure('release_retention_receipt_invalid');
|
|
218
315
|
} catch (error) {
|
|
219
316
|
if (error.code === 'ENOENT') return null;
|
|
@@ -245,7 +342,7 @@ export async function removeInstalledReleases({ releasesRoot, client }, dependen
|
|
|
245
342
|
if (!result.deferred.length) {
|
|
246
343
|
await fs.rm(path.join(root, `.${client}-retention.json`), { force: true });
|
|
247
344
|
// entryReleases checked above that this is a folder in the releases root.
|
|
248
|
-
if (ENTRY[client]) await fs.rm(releaseProcessDirectory(root, client), { recursive: true, force: true });
|
|
345
|
+
if (ENTRY[client] || client === 'codex') await fs.rm(releaseProcessDirectory(root, client), { recursive: true, force: true });
|
|
249
346
|
}
|
|
250
347
|
result.complete = result.deferred.length === 0;
|
|
251
348
|
} catch (error) { result.error = safeCode(error); }
|
|
@@ -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),
|
|
@@ -6,7 +6,7 @@ import { fileURLToPath, pathToFileURL } from "node:url";
|
|
|
6
6
|
const ROOT = path.dirname(fileURLToPath(import.meta.url));
|
|
7
7
|
const PIN_PATH = path.join(ROOT, "neutral-host-release-pin.json");
|
|
8
8
|
const MANIFEST_PATH = path.join(ROOT, "neutral-host-release-manifest.json");
|
|
9
|
-
const SAFE_RUNTIME_RELATIVE = /^(?:[A-Za-z0-9][A-Za-z0-9._-]*\/)*[A-Za-z0-9][A-Za-z0-9._-]*\.(?:cjs|mjs|ps1)$/;
|
|
9
|
+
const SAFE_RUNTIME_RELATIVE = /^(?:[A-Za-z0-9][A-Za-z0-9._-]*\/)*[A-Za-z0-9][A-Za-z0-9._-]*\.(?:cjs|mjs|ps1|swift|bin)$/;
|
|
10
10
|
const SAFE_RELEASE_RELATIVE = /^release\/(?:[A-Za-z0-9][A-Za-z0-9._-]*\/)*[A-Za-z0-9][A-Za-z0-9._-]*\.(?:mjs|ps1|md|html|js|css|json)$/;
|
|
11
11
|
|
|
12
12
|
export async function inspectNeutralHostRelease() {
|