@hellolynk-ai/codex-plugin 0.2.102-macos.1 → 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/bin/hellolynk-client-setup.mjs +7 -2
- package/hellolynk-client-pack-manifest.json +30 -30
- package/hellolynk-client-pack-pin.json +1 -1
- package/package.json +1 -1
- package/plugins/hellolynk-codex/.codex-plugin/plugin.json +1 -1
- package/plugins/hellolynk-codex/package.json +1 -1
- package/plugins/hellolynk-codex/runtime/bootstrap/core/hellolynk-darwin-host.mjs +45 -32
- package/plugins/hellolynk-codex/runtime/core/hellolynk-client-update-entry.mjs +3 -3
- package/plugins/hellolynk-codex/runtime/core/hellolynk-client-update-runtime.mjs +47 -34
- package/plugins/hellolynk-codex/runtime/core/hellolynk-client-update.mjs +46 -16
- package/plugins/hellolynk-codex/runtime/core/hellolynk-darwin-host.mjs +45 -32
- package/plugins/hellolynk-codex/runtime/core/hellolynk-neutral-host-process.mjs +1 -1
- package/plugins/hellolynk-codex/runtime/core/hellolynk-release-retention.mjs +34 -11
- package/plugins/hellolynk-codex/runtime/neutral-host-release-manifest.json +11 -11
- package/plugins/hellolynk-codex/runtime/neutral-host-release-pin.json +1 -1
- package/plugins/hellolynk-codex/runtime/platform/windows/credential-store.ps1 +46 -10
- package/plugins/hellolynk-codex/server/codex-neutral-production.mjs +1 -1
- 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
|
@@ -6,17 +6,24 @@ import { withNeutralHostStoreLock } from "./hellolynk-neutral-host-installation-
|
|
|
6
6
|
|
|
7
7
|
const CATALOG = "https://hellolynk.com/downloads/clients.json";
|
|
8
8
|
export const CLIENT_UPDATE_CLIENTS = Object.freeze(["codex", "openclaw", "trae-work", "workbuddy", "dsh", "claude-code"]);
|
|
9
|
-
export const
|
|
9
|
+
export const MAC_CLIENT_VERSION = /^\d+\.\d+\.\d+-macos\.[1-9]\d*$/u;
|
|
10
10
|
const HOUR = 3_600_000;
|
|
11
11
|
const MAX_ARTIFACT = 64 * 1024 * 1024;
|
|
12
12
|
const HASH = /^[a-f0-9]{64}$/;
|
|
13
|
-
const CONTRACT = "hellolynk-client-update-
|
|
13
|
+
const CONTRACT = "hellolynk-client-update-v2";
|
|
14
|
+
export const CLIENT_UPDATE_RELEASES = "releases-v2";
|
|
15
|
+
// The release freeze pins only public Ed25519 keys here. An empty set refuses all updates.
|
|
16
|
+
export const CLIENT_UPDATE_CATALOG_KEYS = Object.freeze({
|
|
17
|
+
"ed25519-20261109-primary-d4fd7f46475dac48": "-----BEGIN PUBLIC KEY-----\nMCowBQYDK2VwAyEAucQohi+be+ksjQhRzYD8ZiyZMUz6Vi/DSphQ3iVOWDM=\n-----END PUBLIC KEY-----\n",
|
|
18
|
+
"ed25519-20261109-backup-5e3d08a775320304": "-----BEGIN PUBLIC KEY-----\nMCowBQYDK2VwAyEA2vxHpcgLZQc1cXqxq3dcmZRmE0fAFyeRC3umMVGAosA=\n-----END PUBLIC KEY-----\n"
|
|
19
|
+
});
|
|
20
|
+
export const CLIENT_UPDATE_CATALOG_LIMIT = 256 * 1024;
|
|
14
21
|
// A failed integrity or path check keeps a downloaded package from installing. Every other failure is transient.
|
|
15
|
-
const BLOCKING = /^client_update_(?:same_version_changed|version_rollback|artifact_integrity_invalid|archive_[a-z_]+|path_invalid|url_invalid|redirect_invalid)$/;
|
|
22
|
+
const BLOCKING = /^client_update_(?:catalog_signature_invalid|same_version_changed|version_rollback|artifact_integrity_invalid|archive_[a-z_]+|path_invalid|url_invalid|redirect_invalid)$/;
|
|
16
23
|
|
|
17
24
|
export function clientUpdateCatalogId(client, platform) {
|
|
18
25
|
if (platform === "win32") return client;
|
|
19
|
-
if (platform === "darwin" && client === "codex") return
|
|
26
|
+
if (platform === "darwin" && (client === "codex" || client === "claude-code")) return `${client}-macos`;
|
|
20
27
|
return null;
|
|
21
28
|
}
|
|
22
29
|
|
|
@@ -26,10 +33,12 @@ export function createClientUpdateManager(options = {}) {
|
|
|
26
33
|
const { client, currentVersion, platform = process.platform } = options;
|
|
27
34
|
if (!CLIENT_UPDATE_CLIENTS.includes(client) || !semver(currentVersion) || !path.isAbsolute(String(options.root || ""))) fail("configuration_invalid");
|
|
28
35
|
const catalogId = clientUpdateCatalogId(client, platform);
|
|
29
|
-
const mac =
|
|
30
|
-
if (mac && !
|
|
36
|
+
const mac = platform === "darwin";
|
|
37
|
+
if (mac && !MAC_CLIENT_VERSION.test(currentVersion)) fail("configuration_invalid");
|
|
31
38
|
const root = path.join(path.resolve(options.root), "client-updates", client);
|
|
32
|
-
|
|
39
|
+
// A still-running unsigned updater may read, rewrite and prune its own cache.
|
|
40
|
+
const stateFile = path.join(root, "state-v2.json");
|
|
41
|
+
const releases = path.join(root, CLIENT_UPDATE_RELEASES);
|
|
33
42
|
const fetcher = options.fetchImpl || globalThis.fetch;
|
|
34
43
|
const clock = options.clock || Date.now;
|
|
35
44
|
const timeoutMs = bounded(options.timeoutMs, 30_000, 50, 120_000);
|
|
@@ -70,7 +79,7 @@ export function createClientUpdateManager(options = {}) {
|
|
|
70
79
|
}
|
|
71
80
|
}
|
|
72
81
|
|
|
73
|
-
function releasePath(record) { return path.join(
|
|
82
|
+
function releasePath(record) { return path.join(releases, `${record.version}-${record.sha256}`); }
|
|
74
83
|
async function save(record) {
|
|
75
84
|
await safeDirectory(root);
|
|
76
85
|
const temporary = path.join(root, `.state-${crypto.randomUUID()}.tmp`);
|
|
@@ -92,10 +101,11 @@ export function createClientUpdateManager(options = {}) {
|
|
|
92
101
|
if (!force && old.checkedAt > 0 && now >= old.checkedAt && now - old.checkedAt < 23 * HOUR) return old;
|
|
93
102
|
if (!catalogId) return save({ ...old, stage: "unsupported", checkedAt: now });
|
|
94
103
|
try {
|
|
95
|
-
const metadata = await request(CATALOG, { limit:
|
|
104
|
+
const metadata = await request(CATALOG, { limit: CLIENT_UPDATE_CATALOG_LIMIT, timeout: timeoutMs, kind: "catalog" });
|
|
96
105
|
let catalog;
|
|
97
106
|
try { catalog = JSON.parse(metadata.toString("utf8")); } catch { fail("catalog_invalid"); }
|
|
98
|
-
const
|
|
107
|
+
const verified = verifyClientUpdateCatalog(catalog, options.catalogKeys);
|
|
108
|
+
const artifact = validateClientUpdateCatalogEntry(verified, catalogId, mac);
|
|
99
109
|
// The catalog has no build for this client: there is nothing to install.
|
|
100
110
|
if (!artifact) {
|
|
101
111
|
// A downloaded package keeps its stage and any integrity block; only an empty cache is "current".
|
|
@@ -144,7 +154,7 @@ export function createClientUpdateManager(options = {}) {
|
|
|
144
154
|
if (!bytes) fail("source_unavailable");
|
|
145
155
|
const release = releasePath(artifact);
|
|
146
156
|
await safeDirectory(path.dirname(release));
|
|
147
|
-
const stage = path.join(
|
|
157
|
+
const stage = path.join(releases, `.stage-${crypto.randomUUID()}`);
|
|
148
158
|
await fs.mkdir(stage, { mode: 0o700 });
|
|
149
159
|
try {
|
|
150
160
|
const archivePath = path.join(stage, artifact.filename);
|
|
@@ -157,7 +167,7 @@ export function createClientUpdateManager(options = {}) {
|
|
|
157
167
|
return;
|
|
158
168
|
} catch { /* Preserve damaged evidence without reusing it. */ }
|
|
159
169
|
await safeAncestors(release, true);
|
|
160
|
-
const quarantine = path.join(
|
|
170
|
+
const quarantine = path.join(releases, `.corrupt-${artifact.sha256}-${crypto.randomUUID()}`);
|
|
161
171
|
await fs.rename(release, quarantine);
|
|
162
172
|
try { await fs.rename(stage, release); }
|
|
163
173
|
catch (replacement) { await fs.rename(quarantine, release).catch(() => {}); throw replacement; }
|
|
@@ -182,7 +192,7 @@ export function createClientUpdateManager(options = {}) {
|
|
|
182
192
|
});
|
|
183
193
|
}
|
|
184
194
|
|
|
185
|
-
// After an install only the package state
|
|
195
|
+
// After an install only the package the signed state names stays cached; older
|
|
186
196
|
// versions and quarantined copies go. Holding the check lock means no
|
|
187
197
|
// download is in flight to be mistaken for leftovers.
|
|
188
198
|
async function prune() {
|
|
@@ -190,7 +200,7 @@ export function createClientUpdateManager(options = {}) {
|
|
|
190
200
|
return withNeutralHostStoreLock(root, "client-update", async () => {
|
|
191
201
|
const state = await readState();
|
|
192
202
|
if (!semver(state.version) || !HASH.test(String(state.sha256 || ""))) return [];
|
|
193
|
-
const keep = path.basename(releasePath(state)),
|
|
203
|
+
const keep = path.basename(releasePath(state)), removed = [];
|
|
194
204
|
await safeAncestors(releases, true);
|
|
195
205
|
let entries;
|
|
196
206
|
try { entries = await fs.readdir(releases, { withFileTypes: true }); }
|
|
@@ -277,13 +287,33 @@ export function createClientUpdateManager(options = {}) {
|
|
|
277
287
|
});
|
|
278
288
|
}
|
|
279
289
|
|
|
280
|
-
function
|
|
290
|
+
export function verifyClientUpdateCatalog(catalog, keys = CLIENT_UPDATE_CATALOG_KEYS) {
|
|
291
|
+
const signed = catalog?.signed;
|
|
292
|
+
if (!signed || typeof signed.keyId !== "string" || !/^[A-Za-z0-9_-]{1,64}$/.test(signed.keyId)
|
|
293
|
+
|| !Object.hasOwn(keys, signed.keyId) || typeof signed.payload !== "string" || !signed.payload
|
|
294
|
+
|| signed.payload.length > CLIENT_UPDATE_CATALOG_LIMIT || typeof signed.signature !== "string"
|
|
295
|
+
|| !/^[A-Za-z0-9_-]{86}$/.test(signed.signature)) fail("catalog_signature_invalid");
|
|
296
|
+
const payload = Buffer.from(signed.payload, "base64");
|
|
297
|
+
const signature = Buffer.from(signed.signature, "base64url");
|
|
298
|
+
if (payload.toString("base64") !== signed.payload || signature.toString("base64url") !== signed.signature) fail("catalog_signature_invalid");
|
|
299
|
+
let valid = false;
|
|
300
|
+
try {
|
|
301
|
+
const key = crypto.createPublicKey(keys[signed.keyId]);
|
|
302
|
+
valid = key.asymmetricKeyType === "ed25519" && crypto.verify(null, payload, key, signature);
|
|
303
|
+
} catch { /* Invalid pins and invalid signatures both fail closed. */ }
|
|
304
|
+
if (!valid) fail("catalog_signature_invalid");
|
|
305
|
+
const json = payload.toString("utf8");
|
|
306
|
+
if (!Buffer.from(json, "utf8").equals(payload)) fail("catalog_invalid");
|
|
307
|
+
try { return JSON.parse(json); } catch { fail("catalog_invalid"); }
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
export function validateClientUpdateCatalogEntry(catalog, client, mac) {
|
|
281
311
|
if (!catalog || !Array.isArray(catalog.clients) || catalog.clients.length > 32) fail("catalog_invalid");
|
|
282
312
|
const matches = catalog.clients.filter((item) => item?.client === client);
|
|
283
313
|
if (matches.length === 0) return null;
|
|
284
314
|
if (matches.length > 1) fail("catalog_invalid");
|
|
285
315
|
const item = matches[0];
|
|
286
|
-
if (!semver(item.version) || (mac && !
|
|
316
|
+
if (!semver(item.version) || (mac && !MAC_CLIENT_VERSION.test(item.version))
|
|
287
317
|
|| !HASH.test(String(item.sha256 || "")) || !safeFilename(item.filename)
|
|
288
318
|
|| !Number.isSafeInteger(item.bytes) || item.bytes < 4 || item.bytes > MAX_ARTIFACT
|
|
289
319
|
|| !Array.isArray(item.platforms) || !item.platforms.includes(mac ? "macOS" : "Windows")) fail("catalog_invalid");
|
|
@@ -12,13 +12,13 @@ function unique(records, missing, ambiguous) {
|
|
|
12
12
|
if (records.length !== 1) fail(records.length ? ambiguous : missing);
|
|
13
13
|
return records[0];
|
|
14
14
|
}
|
|
15
|
-
function checkBindingPaths(b) {
|
|
15
|
+
function checkBindingPaths(b, code) {
|
|
16
16
|
try {
|
|
17
17
|
checkDarwinPath(b.runtimeRoot, { directory: true });
|
|
18
18
|
checkDarwinPath(b.releaseRoot, { directory: true });
|
|
19
19
|
if (checkDarwinPath(b.profileManifestPath).size > 65536 ||
|
|
20
|
-
checkDarwinPath(join(b.releaseRoot, b.runnerRelativePath)).size > 1048576) fail(
|
|
21
|
-
} catch { fail(
|
|
20
|
+
checkDarwinPath(join(b.releaseRoot, b.runnerRelativePath)).size > 1048576) fail(code);
|
|
21
|
+
} catch { fail(code); }
|
|
22
22
|
}
|
|
23
23
|
function capMatches(cap, b) {
|
|
24
24
|
return cap.state === "active" && fieldsMatch(cap, b,
|
|
@@ -38,51 +38,60 @@ function revokeCapability(s, r) {
|
|
|
38
38
|
}
|
|
39
39
|
return { capabilityId: r.capabilityId, revokedAt: r.revokedAt, state: "revoked" };
|
|
40
40
|
}
|
|
41
|
-
function
|
|
42
|
-
const
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
41
|
+
function neutralBinding(s, adapter, action, input, oldId) {
|
|
42
|
+
const codex = adapter === "codex";
|
|
43
|
+
const key = codex ? "codex" : "claudeCode";
|
|
44
|
+
const error = suffix => `neutral_${adapter.replace("-", "_")}_${suffix}`;
|
|
45
|
+
const check = binding => checkBindingPaths(binding, error("launch_manifest_invalid"));
|
|
46
|
+
const b = s[key];
|
|
47
|
+
if (action === "Resolve" || action === "Verify") {
|
|
48
|
+
if (!b) fail(error("binding_configuration_required"));
|
|
49
|
+
// Claude retains its identity across uninstall; Verify can inspect a
|
|
50
|
+
// retained binding whose old release is gone. Resolve always needs it.
|
|
51
|
+
if (codex || action === "Resolve") check(b);
|
|
52
|
+
const active = s.caps.filter(c => c.adapterId === adapter && c.state === "active");
|
|
47
53
|
const cap = unique(active.filter(c => capMatches(c, b)),
|
|
48
|
-
active.length ? "
|
|
49
|
-
if (
|
|
54
|
+
error(active.length ? "capability_binding_mismatch" : "capability_missing"), error("capability_ambiguous"));
|
|
55
|
+
if (action === "Resolve") return { binding: b, record: cap };
|
|
50
56
|
const {secret, ...record} = cap;
|
|
51
57
|
return { binding: b, record };
|
|
52
58
|
}
|
|
53
|
-
if (
|
|
54
|
-
|
|
55
|
-
if (
|
|
56
|
-
if (
|
|
57
|
-
|
|
59
|
+
if (action === "Save" || action === "Remove") {
|
|
60
|
+
if (input.adapterId !== adapter) fail(error("binding_invalid"));
|
|
61
|
+
if (action === "Remove") {
|
|
62
|
+
if (codex) check(input);
|
|
63
|
+
if (!equal(b, input)) fail(error("binding_invalid"));
|
|
64
|
+
if (codex) s.codex = null;
|
|
65
|
+
else delete s.claudeCode;
|
|
58
66
|
return { removed: true };
|
|
59
67
|
}
|
|
60
|
-
|
|
61
|
-
|
|
68
|
+
check(input);
|
|
69
|
+
if (b && !equal(b, input)) fail(error("binding_invalid"));
|
|
70
|
+
s[key] = input;
|
|
62
71
|
return { binding: input };
|
|
63
72
|
}
|
|
64
73
|
const { previousBinding: previous, replacementBinding: next, replacementRecord } = input;
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
if (!fieldsMatch(previous, next, ["contract","region","principalId","delegateId","hostInstallationId","profileId","adapterId","version","runtimeRoot"])) {
|
|
68
|
-
fail("
|
|
74
|
+
if (codex) check(previous);
|
|
75
|
+
check(next);
|
|
76
|
+
if (next.adapterId !== adapter || !fieldsMatch(previous, next, ["contract","region","principalId","delegateId","hostInstallationId","profileId","adapterId","version","runtimeRoot"])) {
|
|
77
|
+
fail(error("capability_binding_mismatch"));
|
|
69
78
|
}
|
|
70
|
-
if (!equal(b, previous)) fail("
|
|
79
|
+
if (!equal(b, previous)) fail(error("binding_invalid"));
|
|
71
80
|
const active = s.caps.filter(c => capMatches(c, next));
|
|
72
|
-
if (
|
|
73
|
-
if (!equal(previous.scopes, next.scopes)) fail("
|
|
74
|
-
if (active.length !== 1) fail("
|
|
75
|
-
s
|
|
81
|
+
if (codex && action === "Replace") {
|
|
82
|
+
if (!equal(previous.scopes, next.scopes)) fail(error("capability_binding_mismatch"));
|
|
83
|
+
if (active.length !== 1) fail(error("capability_ambiguous"));
|
|
84
|
+
s[key] = next;
|
|
76
85
|
return { binding: next };
|
|
77
86
|
}
|
|
78
|
-
if (
|
|
87
|
+
if (action !== "Migrate") fail("host_protected_store_command_invalid");
|
|
79
88
|
const old = s.caps.find(c => c.capabilityId === oldId);
|
|
80
|
-
if (!old || !capMatches(old, previous) || !capMatches(replacementRecord, next)) fail("
|
|
89
|
+
if (!old || !capMatches(old, previous) || !capMatches(replacementRecord, next)) fail(error("capability_binding_mismatch"));
|
|
81
90
|
const other = active.filter(c => c.capabilityId !== oldId);
|
|
82
|
-
if (other.length > 1) fail("
|
|
91
|
+
if (other.length > 1) fail(error("capability_ambiguous"));
|
|
83
92
|
if (!other.length) addCapability(s, replacementRecord);
|
|
84
93
|
revokeCapability(s, old);
|
|
85
|
-
s
|
|
94
|
+
s[key] = next;
|
|
86
95
|
return { binding: next, record: other[0] || replacementRecord, replacedCapabilityId: oldId };
|
|
87
96
|
}
|
|
88
97
|
|
|
@@ -247,9 +256,13 @@ export function invokeDarwinHost(q) {
|
|
|
247
256
|
case "DisconnectLocalClient":
|
|
248
257
|
for (const cap of s.caps.filter(c => c.adapterId === input.client && c.state === "active")) revokeCapability(s,cap);
|
|
249
258
|
if (input.client === "codex") s.codex = null;
|
|
259
|
+
if (input.client === "claude-code") delete s.claudeCode;
|
|
250
260
|
return {disconnected:true,client:input.client};
|
|
251
261
|
}
|
|
252
|
-
|
|
262
|
+
const bindingCommand = /^(Resolve|Verify|Save|Remove|Replace|Migrate)(Codex|ClaudeCode)NeutralBinding$/.exec(q.command);
|
|
263
|
+
if (bindingCommand) {
|
|
264
|
+
return neutralBinding(s, bindingCommand[2] === "Codex" ? "codex" : "claude-code", bindingCommand[1], input, q.capabilityId);
|
|
265
|
+
}
|
|
253
266
|
if (q.command.includes("Transport")) return transport(s,q,input);
|
|
254
267
|
fail("host_protected_store_command_unsupported");
|
|
255
268
|
});
|
|
@@ -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;
|
|
@@ -95,9 +95,18 @@ export async function runningReleaseNames(releasesRoot) {
|
|
|
95
95
|
export async function entryProcessList(client, releasesRoot, pids = []) {
|
|
96
96
|
if (!ENTRY[client] && client !== 'codex') return [];
|
|
97
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
|
-
await rootPath(releasesRoot);
|
|
100
|
-
|
|
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
|
+
});
|
|
101
110
|
}
|
|
102
111
|
if (process.platform !== 'win32') throw failure('release_retention_process_check_unavailable');
|
|
103
112
|
const root = await rootPath(releasesRoot);
|
|
@@ -130,7 +139,7 @@ async function darwinPs(fields) {
|
|
|
130
139
|
|
|
131
140
|
// Keep command lines inside the process probe. Only a PID, its start time and
|
|
132
141
|
// whether it might still be serving the Codex cache reach the retention logic.
|
|
133
|
-
|
|
142
|
+
function parseDarwinProcesses(stdout) {
|
|
134
143
|
const processes = new Map();
|
|
135
144
|
for (const line of stdout.trim().split('\n')) {
|
|
136
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);
|
|
@@ -139,6 +148,11 @@ export function parseDarwinCodexEntryProcesses(stdout, pids = []) {
|
|
|
139
148
|
const startedAt = Date.parse(`${started} UTC`);
|
|
140
149
|
processes.set(Number(pid), { pid: Number(pid), parent: Number(parent), startedAt: Number.isFinite(startedAt) ? startedAt : null, command });
|
|
141
150
|
}
|
|
151
|
+
return processes;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export function parseDarwinCodexEntryProcesses(stdout, pids = []) {
|
|
155
|
+
const processes = parseDarwinProcesses(stdout);
|
|
142
156
|
const wanted = new Set(pids);
|
|
143
157
|
const proxy = /(?:^|["\s/])(?:\.\/)?server\/mcp-proxy\.mjs(?=["\s]|$)/u;
|
|
144
158
|
const codexApp = /(?:^|["/])(?:Codex|ChatGPT)\.app\/Contents\//iu;
|
|
@@ -198,19 +212,28 @@ async function entryReleases(releasesRoot, root, client, pattern, dependencies)
|
|
|
198
212
|
// live processes can delete its executable before the next client startup.
|
|
199
213
|
export async function configuredReleaseNames(releasesRoot) {
|
|
200
214
|
if (process.platform === 'darwin') {
|
|
201
|
-
const
|
|
215
|
+
const root = await rootPath(releasesRoot);
|
|
216
|
+
const pattern = darwinReleasePattern(root, releasesRoot);
|
|
202
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
|
+
}
|
|
203
231
|
for (const directory of [path.join(homedir(), 'Library', 'LaunchAgents'), '/Library/LaunchAgents', '/Library/LaunchDaemons']) {
|
|
204
232
|
let entries;
|
|
205
233
|
try { entries = await fs.readdir(directory); }
|
|
206
234
|
catch (error) { if (error.code === 'ENOENT') continue; throw error; }
|
|
207
235
|
for (const entry of entries.filter(name => /hellolynk/i.test(name) && name.endsWith('.plist'))) {
|
|
208
|
-
|
|
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
|
-
}
|
|
236
|
+
await readAgent(path.join(directory, entry));
|
|
214
237
|
}
|
|
215
238
|
}
|
|
216
239
|
return [...names].sort();
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
"files": [
|
|
5
5
|
{
|
|
6
6
|
"path": "release/package.json",
|
|
7
|
-
"sha256": "
|
|
7
|
+
"sha256": "f786baab2e9f35836813c6b77e1cbc0fb8c0a3c0a2055b2e6ea777f2428cd232"
|
|
8
8
|
},
|
|
9
9
|
{
|
|
10
10
|
"path": "neutral-host-bootstrap.mjs",
|
|
@@ -40,19 +40,19 @@
|
|
|
40
40
|
},
|
|
41
41
|
{
|
|
42
42
|
"path": "core/hellolynk-client-update-entry.mjs",
|
|
43
|
-
"sha256": "
|
|
43
|
+
"sha256": "d9548eacda8632976e42aafc65f0cef2ec9c5233d2d53b31b02aec0a1702b5eb"
|
|
44
44
|
},
|
|
45
45
|
{
|
|
46
46
|
"path": "core/hellolynk-client-update-runtime.mjs",
|
|
47
|
-
"sha256": "
|
|
47
|
+
"sha256": "aa233e2cdbdfadb7da6aa489591bc12c4121101b94696f93f6e6cb5d3e7c7870"
|
|
48
48
|
},
|
|
49
49
|
{
|
|
50
50
|
"path": "core/hellolynk-client-update.mjs",
|
|
51
|
-
"sha256": "
|
|
51
|
+
"sha256": "e57f7cc5e98519a014cef7cef68a97bb7e2beb33eb381c38d1751a3967904aaa"
|
|
52
52
|
},
|
|
53
53
|
{
|
|
54
54
|
"path": "core/hellolynk-release-retention.mjs",
|
|
55
|
-
"sha256": "
|
|
55
|
+
"sha256": "0ee150af5a9bcc1f636e3ff980ee90ce7d073ab5ed4a991920cd3472bc65529c"
|
|
56
56
|
},
|
|
57
57
|
{
|
|
58
58
|
"path": "core/hellolynk-contact-request-e2ee.mjs",
|
|
@@ -64,7 +64,7 @@
|
|
|
64
64
|
},
|
|
65
65
|
{
|
|
66
66
|
"path": "core/hellolynk-darwin-host.mjs",
|
|
67
|
-
"sha256": "
|
|
67
|
+
"sha256": "ded97c3bc3dd65ca75305f5199a1340dffb987a5f283454d60586df8b6b55c09"
|
|
68
68
|
},
|
|
69
69
|
{
|
|
70
70
|
"path": "core/hellolynk-darwin-vault.mjs",
|
|
@@ -196,7 +196,7 @@
|
|
|
196
196
|
},
|
|
197
197
|
{
|
|
198
198
|
"path": "core/hellolynk-neutral-host-process.mjs",
|
|
199
|
-
"sha256": "
|
|
199
|
+
"sha256": "7b237a1400c604da5decbb29ab3ce556fd1b744d0cfd7d28b5560b217da120b1"
|
|
200
200
|
},
|
|
201
201
|
{
|
|
202
202
|
"path": "core/hellolynk-neutral-host-store-adapters.mjs",
|
|
@@ -292,7 +292,7 @@
|
|
|
292
292
|
},
|
|
293
293
|
{
|
|
294
294
|
"path": "platform/windows/credential-store.ps1",
|
|
295
|
-
"sha256": "
|
|
295
|
+
"sha256": "8f288c91a1f6ecaf2651baab9bdfc55014c11ea9e165f203ca7a69f4d47d7917"
|
|
296
296
|
},
|
|
297
297
|
{
|
|
298
298
|
"path": "platform/windows/hellolynk-host-protected-store.ps1",
|
|
@@ -312,7 +312,7 @@
|
|
|
312
312
|
},
|
|
313
313
|
{
|
|
314
314
|
"path": "bootstrap/core/hellolynk-darwin-host.mjs",
|
|
315
|
-
"sha256": "
|
|
315
|
+
"sha256": "ded97c3bc3dd65ca75305f5199a1340dffb987a5f283454d60586df8b6b55c09"
|
|
316
316
|
},
|
|
317
317
|
{
|
|
318
318
|
"path": "bootstrap/core/hellolynk-darwin-vault.mjs",
|
|
@@ -376,7 +376,7 @@
|
|
|
376
376
|
},
|
|
377
377
|
{
|
|
378
378
|
"path": "release/server/codex-neutral-production.mjs",
|
|
379
|
-
"sha256": "
|
|
379
|
+
"sha256": "2c2f209339385cc14f27fe985f4deb75f4bd48e689035311847cc3b08e36ac52"
|
|
380
380
|
},
|
|
381
381
|
{
|
|
382
382
|
"path": "release/server/neutral-host-adapter.mjs",
|
|
@@ -384,7 +384,7 @@
|
|
|
384
384
|
},
|
|
385
385
|
{
|
|
386
386
|
"path": "release/server/workbench-runtime.mjs",
|
|
387
|
-
"sha256": "
|
|
387
|
+
"sha256": "1136c7ea50cc73065fe04094c5b5c19cb676c40a0a189979015766a9c4e908d8"
|
|
388
388
|
}
|
|
389
389
|
]
|
|
390
390
|
}
|
|
@@ -85,8 +85,27 @@ public static class HelloLynkWindowsFileNative {
|
|
|
85
85
|
[DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
|
|
86
86
|
public static extern bool MoveFileExW(string existingName, string newName, uint flags);
|
|
87
87
|
|
|
88
|
+
[StructLayout(LayoutKind.Sequential)]
|
|
89
|
+
private struct SECURITY_ATTRIBUTES {
|
|
90
|
+
public int Length;
|
|
91
|
+
public IntPtr SecurityDescriptor;
|
|
92
|
+
public int InheritHandle;
|
|
93
|
+
}
|
|
94
|
+
|
|
88
95
|
[DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
|
|
89
|
-
|
|
96
|
+
private static extern bool CreateDirectoryW(string path, ref SECURITY_ATTRIBUTES attributes);
|
|
97
|
+
|
|
98
|
+
public static bool CreateProtectedDirectory(string path, byte[] descriptor) {
|
|
99
|
+
var pinned = GCHandle.Alloc(descriptor, GCHandleType.Pinned);
|
|
100
|
+
try {
|
|
101
|
+
var attributes = new SECURITY_ATTRIBUTES {
|
|
102
|
+
Length = Marshal.SizeOf<SECURITY_ATTRIBUTES>(),
|
|
103
|
+
SecurityDescriptor = pinned.AddrOfPinnedObject(),
|
|
104
|
+
InheritHandle = 0
|
|
105
|
+
};
|
|
106
|
+
return CreateDirectoryW(path, ref attributes);
|
|
107
|
+
} finally { pinned.Free(); }
|
|
108
|
+
}
|
|
90
109
|
}
|
|
91
110
|
'@
|
|
92
111
|
|
|
@@ -162,7 +181,7 @@ function Get-HelloLynkExistingAncestorDirectories([string]$RootPath) {
|
|
|
162
181
|
return $ordered
|
|
163
182
|
}
|
|
164
183
|
|
|
165
|
-
function Assert-HelloLynkTrustedAncestorAcl([string]$RootPath) {
|
|
184
|
+
function Assert-HelloLynkTrustedAncestorAcl([string]$RootPath, [bool]$Create = $false) {
|
|
166
185
|
$rootParent = [IO.Path]::GetDirectoryName($RootPath.TrimEnd([char[]]@('\', '/')))
|
|
167
186
|
$directories = @(Get-HelloLynkExistingAncestorDirectories $RootPath)
|
|
168
187
|
$dangerousRights = (
|
|
@@ -175,6 +194,8 @@ function Assert-HelloLynkTrustedAncestorAcl([string]$RootPath) {
|
|
|
175
194
|
[Security.AccessControl.FileSystemRights]::ChangePermissions -bor
|
|
176
195
|
[Security.AccessControl.FileSystemRights]::TakeOwnership
|
|
177
196
|
)
|
|
197
|
+
$createRights = [Security.AccessControl.FileSystemRights]::CreateFiles -bor
|
|
198
|
+
[Security.AccessControl.FileSystemRights]::CreateDirectories
|
|
178
199
|
foreach ($directory in $directories) {
|
|
179
200
|
Assert-HelloLynkNoReparseAncestors $directory
|
|
180
201
|
$identity = Open-HelloLynkIdentity $directory $true
|
|
@@ -204,6 +225,7 @@ function Assert-HelloLynkTrustedAncestorAcl([string]$RootPath) {
|
|
|
204
225
|
)
|
|
205
226
|
$isRootParent = $directory.Equals($rootParent, [StringComparison]::OrdinalIgnoreCase)
|
|
206
227
|
$directoryIndex = [Array]::IndexOf($directories, $directory)
|
|
228
|
+
$willCreateProtectedChild = $Create -and -not $isRootParent -and $directoryIndex -eq $directories.Count - 1
|
|
207
229
|
$nextDirectoryProtected = $false
|
|
208
230
|
if ($directoryIndex -ge 0 -and $directoryIndex + 1 -lt $directories.Count) {
|
|
209
231
|
try {
|
|
@@ -220,15 +242,20 @@ function Assert-HelloLynkTrustedAncestorAcl([string]$RootPath) {
|
|
|
220
242
|
) { continue }
|
|
221
243
|
$rights = $rule.FileSystemRights
|
|
222
244
|
$hasDangerousRights = (($rights -band $dangerousRights) -ne 0)
|
|
245
|
+
# A competing child created by a different user fails our owner check.
|
|
246
|
+
# A grant to alter the ancestor itself still prevents first creation.
|
|
247
|
+
$canCreateProtectedChild = $willCreateProtectedChild -and
|
|
248
|
+
(($rights -band $dangerousRights -band (-bnot $createRights)) -eq 0)
|
|
223
249
|
$hasDeleteChild = (
|
|
224
250
|
($rights -band [Security.AccessControl.FileSystemRights]::DeleteSubdirectoriesAndFiles) -ne 0
|
|
225
251
|
)
|
|
226
252
|
if (
|
|
227
253
|
$hasDeleteChild -or
|
|
228
|
-
($hasDangerousRights -and ($isRootParent -or -not $nextDirectoryProtected))
|
|
254
|
+
($hasDangerousRights -and ($isRootParent -or (-not $nextDirectoryProtected -and -not $canCreateProtectedChild)))
|
|
229
255
|
) { Fail "client_root_ancestor_dacl_untrusted" }
|
|
230
256
|
}
|
|
231
257
|
}
|
|
258
|
+
return $directories[-1]
|
|
232
259
|
}
|
|
233
260
|
|
|
234
261
|
function New-HelloLynkAcl([bool]$Directory) {
|
|
@@ -361,7 +388,7 @@ function Exit-HelloLynkRootMutex($Mutex) {
|
|
|
361
388
|
try { $Mutex.ReleaseMutex() } finally { $Mutex.Dispose() }
|
|
362
389
|
}
|
|
363
390
|
|
|
364
|
-
function Ensure-HelloLynkSecureDirectory([string]$PathValue) {
|
|
391
|
+
function Ensure-HelloLynkSecureDirectory([string]$PathValue, [string]$ExistingAncestor = "") {
|
|
365
392
|
if ([IO.Directory]::Exists($PathValue)) {
|
|
366
393
|
Assert-HelloLynkNoReparseAncestors $PathValue
|
|
367
394
|
Assert-HelloLynkSecureAcl $PathValue $true
|
|
@@ -370,12 +397,21 @@ function Ensure-HelloLynkSecureDirectory([string]$PathValue) {
|
|
|
370
397
|
return
|
|
371
398
|
}
|
|
372
399
|
$parent = [IO.Path]::GetDirectoryName($PathValue)
|
|
373
|
-
if (
|
|
374
|
-
|
|
400
|
+
if ($ExistingAncestor) {
|
|
401
|
+
if (-not $parent.Equals($ExistingAncestor, [StringComparison]::OrdinalIgnoreCase)) {
|
|
402
|
+
Ensure-HelloLynkSecureDirectory $parent $ExistingAncestor
|
|
403
|
+
}
|
|
404
|
+
} elseif (-not [IO.Directory]::Exists($parent)) {
|
|
405
|
+
Ensure-HelloLynkSecureDirectory $parent
|
|
406
|
+
}
|
|
407
|
+
Assert-HelloLynkNoReparseAncestors $PathValue
|
|
408
|
+
$descriptor = (New-HelloLynkAcl $true).GetSecurityDescriptorBinaryForm()
|
|
409
|
+
if (-not [HelloLynkWindowsFileNative]::CreateProtectedDirectory($PathValue, $descriptor)) {
|
|
375
410
|
Fail "client_root_create_race"
|
|
376
411
|
}
|
|
377
|
-
Set-HelloLynkSecureAcl $PathValue $true
|
|
378
412
|
Assert-HelloLynkNoReparseAncestors $PathValue
|
|
413
|
+
# Keep the Windows auto-inheritance marker used by child file ACL checks.
|
|
414
|
+
Set-HelloLynkSecureAcl $PathValue $true
|
|
379
415
|
Assert-HelloLynkSecureAcl $PathValue $true
|
|
380
416
|
$identity = Open-HelloLynkIdentity $PathValue $true
|
|
381
417
|
$identity.Handle.Dispose()
|
|
@@ -386,14 +422,14 @@ function Resolve-HelloLynkClientRoot([string]$RootValue, [bool]$Create) {
|
|
|
386
422
|
Assert-HelloLynkNoReparseAncestors $root
|
|
387
423
|
$mutex = Enter-HelloLynkRootMutex $root
|
|
388
424
|
try {
|
|
389
|
-
Assert-HelloLynkTrustedAncestorAcl $root
|
|
390
|
-
if ($Create) { Ensure-HelloLynkSecureDirectory $root }
|
|
425
|
+
$existingAncestor = Assert-HelloLynkTrustedAncestorAcl $root $Create
|
|
426
|
+
if ($Create) { Ensure-HelloLynkSecureDirectory $root $existingAncestor }
|
|
391
427
|
if (-not [IO.Directory]::Exists($root)) { Fail "client_root_missing" }
|
|
392
428
|
Assert-HelloLynkNoReparseAncestors $root
|
|
393
429
|
Assert-HelloLynkSecureAcl $root $true
|
|
394
430
|
$identity = Open-HelloLynkIdentity $root $true
|
|
395
431
|
try {
|
|
396
|
-
Assert-HelloLynkTrustedAncestorAcl $root
|
|
432
|
+
$null = Assert-HelloLynkTrustedAncestorAcl $root
|
|
397
433
|
$canonical = $root
|
|
398
434
|
} finally { $identity.Handle.Dispose() }
|
|
399
435
|
return [pscustomobject]@{ Root = $canonical; Mutex = $mutex }
|
|
@@ -24,7 +24,7 @@ const TRUSTED_BOOTSTRAP_FILES = Object.freeze(new Map([
|
|
|
24
24
|
["runtime/neutral-host-release-loader.mjs", "283a20c6a9b90481bc03c143ab51d5289abf7b7a86fbe51fba7ea2ae1e6d2c60"],
|
|
25
25
|
["runtime/neutral-host-bootstrap.mjs", "53513deed7f0155a6603a1489d1609fae37776442358181ff7650d0cd9351f0a"],
|
|
26
26
|
["runtime/bootstrap/core/hellolynk-runtime-safety.cjs", "43d576d9f774409f4a735793c1503a6f397e787b10c59a4ac9cc9257299f5efe"],
|
|
27
|
-
["runtime/bootstrap/core/hellolynk-darwin-host.mjs", "
|
|
27
|
+
["runtime/bootstrap/core/hellolynk-darwin-host.mjs", "ded97c3bc3dd65ca75305f5199a1340dffb987a5f283454d60586df8b6b55c09"],
|
|
28
28
|
["runtime/bootstrap/core/hellolynk-darwin-vault.mjs", "b6094d856af016e674e4a0183a4e474397bbe92e66638743fa62fe085b554d30"],
|
|
29
29
|
["runtime/bootstrap/core/hellolynk-host-protected-store.mjs", "10f81f4f6a5dd2fe9a742e847b2eb47dbf20f802f4414f46a05ccbc3505148e2"],
|
|
30
30
|
["runtime/bootstrap/core/hellolynk-neutral-host-installation-store.mjs", "42ae1e7deb876a8308b7bfed6a7b891d1834117b62504fc5befe3f91a0b4c740"],
|