codex-copilot-dx 0.6.14 → 0.6.18
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 +30 -16
- package/bin/cli.mjs +5 -0
- package/package.json +1 -1
- package/src/adapter.mjs +34 -9
- package/src/anthropic-handler.mjs +54 -4
- package/src/anthropic.mjs +64 -26
- package/src/atomic-file.mjs +76 -2
- package/src/auth-profile.mjs +11 -34
- package/src/auth.mjs +265 -52
- package/src/cli-auth.mjs +28 -71
- package/src/cli-options.mjs +8 -5
- package/src/copilot-client.mjs +5 -1
- package/src/copilot-responses-compat.mjs +80 -0
- package/src/copilot-responses-policy.mjs +78 -0
- package/src/copilot.mjs +2 -1
- package/src/http-transport.mjs +91 -0
- package/src/lock.mjs +179 -17
- package/src/model-cache.mjs +36 -6
- package/src/models.mjs +17 -0
- package/src/observability.mjs +13 -1
- package/src/pm-studio-asar.mjs +399 -19
- package/src/pm-studio-relay.mjs +6 -3
- package/src/pm-studio-setup.mjs +319 -14
- package/src/pm-studio-status.mjs +5 -1
- package/src/profile-model-runtime.mjs +87 -13
- package/src/profile-routing.mjs +45 -7
- package/src/response-history.mjs +142 -16
- package/src/responses-bridge.mjs +22 -4
- package/src/responses-handler.mjs +154 -39
- package/src/responses-image-pressure.mjs +30 -0
- package/src/responses-proxy.mjs +10 -2
- package/src/responses-request.mjs +109 -103
- package/src/route-plan.mjs +51 -0
- package/src/startup.mjs +8 -2
- package/src/usage.mjs +25 -7
package/src/pm-studio-asar.mjs
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
+
import fs from "node:fs";
|
|
2
3
|
|
|
3
4
|
export const PM_STUDIO_ORIGIN = "https://api.githubcopilot.com";
|
|
4
5
|
export const CCDX_PM_STUDIO_ORIGIN = "http://127.0.0.1:2026/pm-ccdx";
|
|
@@ -272,6 +273,187 @@ function aligned4(value) {
|
|
|
272
273
|
return (value + 3) & ~3;
|
|
273
274
|
}
|
|
274
275
|
|
|
276
|
+
const FILE_READ_CHUNK_BYTES = 1024 * 1024;
|
|
277
|
+
const PM_STUDIO_ASAR_HEADER_MAX_BYTES = 64 * 1024 * 1024;
|
|
278
|
+
|
|
279
|
+
function fileIdentity(filePath, stat) {
|
|
280
|
+
return [
|
|
281
|
+
filePath,
|
|
282
|
+
stat.dev,
|
|
283
|
+
stat.ino,
|
|
284
|
+
stat.size,
|
|
285
|
+
stat.mtimeNs,
|
|
286
|
+
stat.ctimeNs,
|
|
287
|
+
].join("\0");
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function readExact(file, buffer, position, io) {
|
|
291
|
+
let offset = 0;
|
|
292
|
+
while (offset < buffer.length) {
|
|
293
|
+
const length = io.readSync(file, buffer, offset, buffer.length - offset, position + offset);
|
|
294
|
+
if (length === 0) throw new Error("Invalid ASAR: header is truncated");
|
|
295
|
+
offset += length;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function sameFileSnapshot(left, right) {
|
|
300
|
+
return left.dev === right.dev
|
|
301
|
+
&& left.ino === right.ino
|
|
302
|
+
&& left.size === right.size
|
|
303
|
+
&& left.mtimeNs === right.mtimeNs
|
|
304
|
+
&& left.ctimeNs === right.ctimeNs;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function withVerifiedFile(filePath, {
|
|
308
|
+
expectedIdentity = "",
|
|
309
|
+
io = fs,
|
|
310
|
+
label,
|
|
311
|
+
} = {}, read) {
|
|
312
|
+
const realPath = io.realpathSync(filePath);
|
|
313
|
+
const pathBefore = io.statSync(realPath, { bigint: true });
|
|
314
|
+
const file = io.openSync(realPath, io.constants.O_RDONLY | (io.constants.O_NOFOLLOW || 0));
|
|
315
|
+
try {
|
|
316
|
+
const before = io.fstatSync(file, { bigint: true });
|
|
317
|
+
const identity = fileIdentity(realPath, before);
|
|
318
|
+
if (!sameFileSnapshot(pathBefore, before) || (expectedIdentity && expectedIdentity !== identity)) {
|
|
319
|
+
throw new Error(`PM Studio ${label} changed before it could be read: ${filePath}`);
|
|
320
|
+
}
|
|
321
|
+
const result = read({ file, before, realPath });
|
|
322
|
+
const after = io.fstatSync(file, { bigint: true });
|
|
323
|
+
const pathAfter = io.statSync(realPath, { bigint: true });
|
|
324
|
+
if (!sameFileSnapshot(before, after) || !sameFileSnapshot(after, pathAfter)) {
|
|
325
|
+
throw new Error(`PM Studio ${label} changed while reading: ${filePath}`);
|
|
326
|
+
}
|
|
327
|
+
return {
|
|
328
|
+
result,
|
|
329
|
+
realPath,
|
|
330
|
+
identity: fileIdentity(realPath, after),
|
|
331
|
+
};
|
|
332
|
+
} finally {
|
|
333
|
+
io.closeSync(file);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
function populateAsarTargetBytes(evidence, targetPaths, io) {
|
|
338
|
+
const missing = targetPaths.filter((targetPath) => !evidence.parsed.targetBytes.has(targetPath));
|
|
339
|
+
withVerifiedFile(evidence.realPath, {
|
|
340
|
+
expectedIdentity: evidence.identity,
|
|
341
|
+
io,
|
|
342
|
+
label: "ASAR",
|
|
343
|
+
}, ({ file, before }) => {
|
|
344
|
+
for (const targetPath of missing) {
|
|
345
|
+
const entry = findAsarEntry(evidence.parsed.header, targetPath);
|
|
346
|
+
if (!entry || entry.unpacked) continue;
|
|
347
|
+
let offset;
|
|
348
|
+
let size;
|
|
349
|
+
try {
|
|
350
|
+
offset = entryOffset(entry.offset, `${targetPath} offset`);
|
|
351
|
+
size = entrySize(entry.size, `${targetPath} size`);
|
|
352
|
+
} catch {
|
|
353
|
+
continue;
|
|
354
|
+
}
|
|
355
|
+
if (size > PM_STUDIO_COMPATIBLE_TARGET_MAX_BYTES) {
|
|
356
|
+
throw new Error(`Invalid ASAR: ${targetPath} exceeds the safe inspection limit`);
|
|
357
|
+
}
|
|
358
|
+
const absoluteOffset = evidence.parsed.dataOffset + offset;
|
|
359
|
+
if (absoluteOffset + size > Number(before.size)) continue;
|
|
360
|
+
const bytes = Buffer.allocUnsafe(size);
|
|
361
|
+
readExact(file, bytes, absoluteOffset, io);
|
|
362
|
+
evidence.parsed.targetBytes.set(targetPath, bytes);
|
|
363
|
+
}
|
|
364
|
+
});
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
function readAsarFileEvidence(filePath, targetPaths, { cache, io = fs } = {}) {
|
|
368
|
+
const realPath = io.realpathSync(filePath);
|
|
369
|
+
const pathStat = io.statSync(realPath, { bigint: true });
|
|
370
|
+
const paths = [...new Set(targetPaths)].sort();
|
|
371
|
+
const cacheKey = `${fileIdentity(realPath, pathStat)}\0${paths.join("\0")}`;
|
|
372
|
+
if (cache?.has(cacheKey)) {
|
|
373
|
+
const evidence = cache.get(cacheKey);
|
|
374
|
+
populateAsarTargetBytes(evidence, paths, io);
|
|
375
|
+
return evidence;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
const opened = withVerifiedFile(realPath, {
|
|
379
|
+
expectedIdentity: fileIdentity(realPath, pathStat),
|
|
380
|
+
io,
|
|
381
|
+
label: "ASAR",
|
|
382
|
+
}, ({ file, before }) => {
|
|
383
|
+
const prefix = Buffer.allocUnsafe(16);
|
|
384
|
+
readExact(file, prefix, 0, io);
|
|
385
|
+
const headerPickleSize = checkedUInt32(prefix, 4, "header pickle size");
|
|
386
|
+
const dataOffset = 8 + headerPickleSize;
|
|
387
|
+
if (dataOffset > PM_STUDIO_ASAR_HEADER_MAX_BYTES) {
|
|
388
|
+
throw new Error("Invalid ASAR: header exceeds the safe inspection limit");
|
|
389
|
+
}
|
|
390
|
+
if (dataOffset > Number(before.size)) {
|
|
391
|
+
throw new Error("Invalid ASAR: header extends outside the archive");
|
|
392
|
+
}
|
|
393
|
+
const headerRegion = Buffer.allocUnsafe(dataOffset);
|
|
394
|
+
readExact(file, headerRegion, 0, io);
|
|
395
|
+
const parsedHeader = parseAsarBuffer(headerRegion);
|
|
396
|
+
const targetRanges = new Map();
|
|
397
|
+
for (const targetPath of paths) {
|
|
398
|
+
const entry = findAsarEntry(parsedHeader.header, targetPath);
|
|
399
|
+
if (!entry || entry.unpacked) continue;
|
|
400
|
+
let offset;
|
|
401
|
+
let size;
|
|
402
|
+
try {
|
|
403
|
+
offset = entryOffset(entry.offset, `${targetPath} offset`);
|
|
404
|
+
size = entrySize(entry.size, `${targetPath} size`);
|
|
405
|
+
} catch {
|
|
406
|
+
continue;
|
|
407
|
+
}
|
|
408
|
+
if (size > PM_STUDIO_COMPATIBLE_TARGET_MAX_BYTES) {
|
|
409
|
+
throw new Error(`Invalid ASAR: ${targetPath} exceeds the safe inspection limit`);
|
|
410
|
+
}
|
|
411
|
+
const absoluteOffset = parsedHeader.dataOffset + offset;
|
|
412
|
+
if (absoluteOffset + size <= Number(before.size)) {
|
|
413
|
+
targetRanges.set(targetPath, { start: absoluteOffset, end: absoluteOffset + size, bytes: Buffer.allocUnsafe(size) });
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
const hash = createHash("sha256");
|
|
418
|
+
const chunk = Buffer.allocUnsafe(FILE_READ_CHUNK_BYTES);
|
|
419
|
+
let position = 0;
|
|
420
|
+
for (;;) {
|
|
421
|
+
const length = io.readSync(file, chunk, 0, chunk.length, position);
|
|
422
|
+
if (length === 0) break;
|
|
423
|
+
const bytes = chunk.subarray(0, length);
|
|
424
|
+
hash.update(bytes);
|
|
425
|
+
const chunkEnd = position + length;
|
|
426
|
+
for (const range of targetRanges.values()) {
|
|
427
|
+
const start = Math.max(position, range.start);
|
|
428
|
+
const end = Math.min(chunkEnd, range.end);
|
|
429
|
+
if (start < end) bytes.copy(range.bytes, start - range.start, start - position, end - position);
|
|
430
|
+
}
|
|
431
|
+
position = chunkEnd;
|
|
432
|
+
}
|
|
433
|
+
if (position !== Number(before.size)) {
|
|
434
|
+
throw new Error(`PM Studio ASAR changed while reading: ${filePath}`);
|
|
435
|
+
}
|
|
436
|
+
return {
|
|
437
|
+
parsed: {
|
|
438
|
+
...parsedHeader,
|
|
439
|
+
buffer: null,
|
|
440
|
+
headerBytes: null,
|
|
441
|
+
headerSha256: sha256Hex(parsedHeader.headerBytes),
|
|
442
|
+
archiveLength: Number(before.size),
|
|
443
|
+
targetBytes: new Map([...targetRanges].map(([name, range]) => [name, range.bytes])),
|
|
444
|
+
},
|
|
445
|
+
asarSha256: hash.digest("hex"),
|
|
446
|
+
};
|
|
447
|
+
});
|
|
448
|
+
const evidence = {
|
|
449
|
+
...opened.result,
|
|
450
|
+
realPath: opened.realPath,
|
|
451
|
+
identity: opened.identity,
|
|
452
|
+
};
|
|
453
|
+
cache?.set(cacheKey, evidence);
|
|
454
|
+
return evidence;
|
|
455
|
+
}
|
|
456
|
+
|
|
275
457
|
export function parseAsarBuffer(value) {
|
|
276
458
|
const buffer = checkedBuffer(value);
|
|
277
459
|
if (buffer.length < 16) throw new Error("Invalid ASAR: header is truncated");
|
|
@@ -456,8 +638,7 @@ function requiredRecipeString(value, name) {
|
|
|
456
638
|
return value;
|
|
457
639
|
}
|
|
458
640
|
|
|
459
|
-
|
|
460
|
-
const parsed = parseAsarBuffer(value);
|
|
641
|
+
function compatibleRecipeFromEvidence(parsed, sourceTarget, sourceAsarSha256, patchedAsarSha256For, options) {
|
|
461
642
|
const entry = findAsarEntry(parsed.header, PM_STUDIO_COPILOT_CONFIG_MODULE_PATH);
|
|
462
643
|
if (!entry) throw incompatibleAsar(`${PM_STUDIO_COPILOT_CONFIG_MODULE_PATH} is missing`);
|
|
463
644
|
|
|
@@ -473,7 +654,8 @@ export function createCompatiblePmStudioRecipe(value, options = {}) {
|
|
|
473
654
|
throw incompatibleAsar(error.message);
|
|
474
655
|
}
|
|
475
656
|
const absoluteOffset = parsed.dataOffset + targetOffset;
|
|
476
|
-
|
|
657
|
+
const archiveLength = parsed.archiveLength ?? parsed.buffer.length;
|
|
658
|
+
if (entry.unpacked || entry.link || absoluteOffset + targetSize > archiveLength) {
|
|
477
659
|
throw incompatibleAsar(`${PM_STUDIO_COPILOT_CONFIG_MODULE_PATH} is not one packed regular ASAR entry`);
|
|
478
660
|
}
|
|
479
661
|
if (blockSize === 0
|
|
@@ -488,8 +670,9 @@ export function createCompatiblePmStudioRecipe(value, options = {}) {
|
|
|
488
670
|
|| entry.integrity.blocks.length !== expectedBlockCount) {
|
|
489
671
|
throw incompatibleAsar(`${PM_STUDIO_COPILOT_CONFIG_MODULE_PATH} has unsafe integrity block geometry`);
|
|
490
672
|
}
|
|
491
|
-
|
|
492
|
-
|
|
673
|
+
if (!sourceTarget || sourceTarget.length !== targetSize) {
|
|
674
|
+
throw incompatibleAsar(`${PM_STUDIO_COPILOT_CONFIG_MODULE_PATH} is not one packed regular ASAR entry`);
|
|
675
|
+
}
|
|
493
676
|
const sourceTargetSha256 = sha256Hex(sourceTarget);
|
|
494
677
|
const sourceBlocks = blockSha256(sourceTarget, blockSize);
|
|
495
678
|
if (entry.integrity.hash !== sourceTargetSha256
|
|
@@ -519,10 +702,8 @@ export function createCompatiblePmStudioRecipe(value, options = {}) {
|
|
|
519
702
|
replacement.copy(patchedTarget, editOffset);
|
|
520
703
|
const patchedTargetSha256 = sha256Hex(patchedTarget);
|
|
521
704
|
const patchedBlocks = blockSha256(patchedTarget, blockSize);
|
|
522
|
-
const
|
|
523
|
-
|
|
524
|
-
const patchedParsed = parseAsarBuffer(patchedAsar);
|
|
525
|
-
const patchedEntry = findAsarEntry(patchedParsed.header, PM_STUDIO_COPILOT_CONFIG_MODULE_PATH);
|
|
705
|
+
const patchedHeader = JSON.parse(JSON.stringify(parsed.header));
|
|
706
|
+
const patchedEntry = findAsarEntry(patchedHeader, PM_STUDIO_COPILOT_CONFIG_MODULE_PATH);
|
|
526
707
|
patchedEntry.integrity = {
|
|
527
708
|
...patchedEntry.integrity,
|
|
528
709
|
algorithm: "SHA256",
|
|
@@ -530,16 +711,19 @@ export function createCompatiblePmStudioRecipe(value, options = {}) {
|
|
|
530
711
|
blockSize,
|
|
531
712
|
blocks: [...patchedBlocks],
|
|
532
713
|
};
|
|
533
|
-
const patchedHeaderBytes = Buffer.from(JSON.stringify(
|
|
534
|
-
if (patchedHeaderBytes.length !==
|
|
535
|
-
throw incompatibleAsar(`Compatible ASAR header changed size (${
|
|
714
|
+
const patchedHeaderBytes = Buffer.from(JSON.stringify(patchedHeader));
|
|
715
|
+
if (patchedHeaderBytes.length !== parsed.headerLength) {
|
|
716
|
+
throw incompatibleAsar(`Compatible ASAR header changed size (${parsed.headerLength} -> ${patchedHeaderBytes.length})`);
|
|
536
717
|
}
|
|
537
|
-
|
|
718
|
+
const patchedAsarSha256 = patchedAsarSha256For({
|
|
719
|
+
absoluteEditOffset: absoluteOffset + editOffset,
|
|
720
|
+
patchedHeaderBytes,
|
|
721
|
+
replacement,
|
|
722
|
+
});
|
|
538
723
|
|
|
539
724
|
const version = requiredRecipeString(String(options.version || ""), "version");
|
|
540
725
|
const build = requiredRecipeString(String(options.build || ""), "build");
|
|
541
726
|
const bundleIdentifier = requiredRecipeString(options.bundleIdentifier, "bundle identifier");
|
|
542
|
-
const sourceAsarSha256 = sha256Hex(parsed.buffer);
|
|
543
727
|
const identity = sha256Hex(`${PM_STUDIO_COMPATIBILITY_ID}\0${bundleIdentifier}\0${version}\0${build}\0${sourceAsarSha256}`).slice(0, 24);
|
|
544
728
|
const sourceBundleContent = options.sourceBundleContent
|
|
545
729
|
? Object.freeze({
|
|
@@ -588,14 +772,111 @@ export function createCompatiblePmStudioRecipe(value, options = {}) {
|
|
|
588
772
|
integrityKey: requiredRecipeString(options.integrityKey, "ASAR integrity key"),
|
|
589
773
|
dataOffset: parsed.dataOffset,
|
|
590
774
|
sourceAsarSha256,
|
|
591
|
-
sourceHeaderSha256: sha256Hex(parsed.headerBytes),
|
|
592
|
-
patchedAsarSha256
|
|
775
|
+
sourceHeaderSha256: parsed.headerSha256 || sha256Hex(parsed.headerBytes),
|
|
776
|
+
patchedAsarSha256,
|
|
593
777
|
patchedHeaderSha256: sha256Hex(patchedHeaderBytes),
|
|
594
778
|
...(sourceBundleContent ? { sourceBundleContent } : {}),
|
|
595
779
|
targets: Object.freeze([target]),
|
|
596
780
|
});
|
|
597
781
|
}
|
|
598
782
|
|
|
783
|
+
export function createCompatiblePmStudioRecipe(value, options = {}) {
|
|
784
|
+
const parsed = parseAsarBuffer(value);
|
|
785
|
+
const entry = findAsarEntry(parsed.header, PM_STUDIO_COPILOT_CONFIG_MODULE_PATH);
|
|
786
|
+
let sourceTarget = null;
|
|
787
|
+
try {
|
|
788
|
+
const absoluteOffset = parsed.dataOffset
|
|
789
|
+
+ entryOffset(entry?.offset, `${PM_STUDIO_COPILOT_CONFIG_MODULE_PATH} offset`);
|
|
790
|
+
const targetSize = entrySize(entry?.size, `${PM_STUDIO_COPILOT_CONFIG_MODULE_PATH} size`);
|
|
791
|
+
if (entry && !entry.unpacked && absoluteOffset + targetSize <= parsed.buffer.length) {
|
|
792
|
+
sourceTarget = parsed.buffer.subarray(absoluteOffset, absoluteOffset + targetSize);
|
|
793
|
+
}
|
|
794
|
+
} catch {
|
|
795
|
+
// The shared validator below preserves the existing classified error.
|
|
796
|
+
}
|
|
797
|
+
return compatibleRecipeFromEvidence(
|
|
798
|
+
parsed,
|
|
799
|
+
sourceTarget,
|
|
800
|
+
sha256Hex(parsed.buffer),
|
|
801
|
+
({ absoluteEditOffset, patchedHeaderBytes, replacement }) => {
|
|
802
|
+
const patchedAsar = Buffer.from(parsed.buffer);
|
|
803
|
+
replacement.copy(patchedAsar, absoluteEditOffset);
|
|
804
|
+
patchedHeaderBytes.copy(patchedAsar, parsed.headerStart);
|
|
805
|
+
return sha256Hex(patchedAsar);
|
|
806
|
+
},
|
|
807
|
+
options,
|
|
808
|
+
);
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
function hashFileWithReplacements(filePath, expectedIdentity, replacements, io = fs) {
|
|
812
|
+
return withVerifiedFile(filePath, {
|
|
813
|
+
expectedIdentity,
|
|
814
|
+
io,
|
|
815
|
+
label: "ASAR",
|
|
816
|
+
}, ({ file, before }) => {
|
|
817
|
+
const ordered = [...replacements].sort((left, right) => left.start - right.start);
|
|
818
|
+
let cursor = 0;
|
|
819
|
+
for (const item of ordered) {
|
|
820
|
+
if (item.start < cursor || item.end < item.start || item.end > Number(before.size)
|
|
821
|
+
|| item.bytes.length !== item.end - item.start) {
|
|
822
|
+
throw new Error("Invalid patched ASAR hash replacement range");
|
|
823
|
+
}
|
|
824
|
+
cursor = item.end;
|
|
825
|
+
}
|
|
826
|
+
const hash = createHash("sha256");
|
|
827
|
+
const chunk = Buffer.allocUnsafe(FILE_READ_CHUNK_BYTES);
|
|
828
|
+
const hashRange = (start, end) => {
|
|
829
|
+
let position = start;
|
|
830
|
+
while (position < end) {
|
|
831
|
+
const requested = Math.min(chunk.length, end - position);
|
|
832
|
+
const length = io.readSync(file, chunk, 0, requested, position);
|
|
833
|
+
if (length === 0) throw new Error(`PM Studio ASAR changed while reading: ${filePath}`);
|
|
834
|
+
hash.update(chunk.subarray(0, length));
|
|
835
|
+
position += length;
|
|
836
|
+
}
|
|
837
|
+
};
|
|
838
|
+
cursor = 0;
|
|
839
|
+
for (const item of ordered) {
|
|
840
|
+
hashRange(cursor, item.start);
|
|
841
|
+
hash.update(item.bytes);
|
|
842
|
+
cursor = item.end;
|
|
843
|
+
}
|
|
844
|
+
hashRange(cursor, Number(before.size));
|
|
845
|
+
return hash.digest("hex");
|
|
846
|
+
}).result;
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
export function createCompatiblePmStudioRecipeFromFile(filePath, options = {}, fileOptions = {}) {
|
|
850
|
+
const evidence = readAsarFileEvidence(filePath, [PM_STUDIO_COPILOT_CONFIG_MODULE_PATH], fileOptions);
|
|
851
|
+
try {
|
|
852
|
+
return compatibleRecipeFromEvidence(
|
|
853
|
+
evidence.parsed,
|
|
854
|
+
evidence.parsed.targetBytes.get(PM_STUDIO_COPILOT_CONFIG_MODULE_PATH),
|
|
855
|
+
evidence.asarSha256,
|
|
856
|
+
({ absoluteEditOffset, patchedHeaderBytes, replacement }) => hashFileWithReplacements(
|
|
857
|
+
evidence.realPath,
|
|
858
|
+
evidence.identity,
|
|
859
|
+
[
|
|
860
|
+
{
|
|
861
|
+
start: evidence.parsed.headerStart,
|
|
862
|
+
end: evidence.parsed.headerEnd,
|
|
863
|
+
bytes: patchedHeaderBytes,
|
|
864
|
+
},
|
|
865
|
+
{
|
|
866
|
+
start: absoluteEditOffset,
|
|
867
|
+
end: absoluteEditOffset + replacement.length,
|
|
868
|
+
bytes: replacement,
|
|
869
|
+
},
|
|
870
|
+
],
|
|
871
|
+
fileOptions.io || fs,
|
|
872
|
+
),
|
|
873
|
+
options,
|
|
874
|
+
);
|
|
875
|
+
} finally {
|
|
876
|
+
if (fileOptions.releaseTargets) evidence.parsed.targetBytes.clear();
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
|
|
599
880
|
export function inspectElectronAsarIntegritySlots(value) {
|
|
600
881
|
const buffer = checkedBuffer(value);
|
|
601
882
|
const sentinel = Buffer.from(ELECTRON_ASAR_INTEGRITY_SENTINEL);
|
|
@@ -619,6 +900,10 @@ export function inspectElectronAsarIntegritySlots(value) {
|
|
|
619
900
|
});
|
|
620
901
|
searchOffset = offset + sentinel.length;
|
|
621
902
|
}
|
|
903
|
+
return electronSlotResult(slots, truncated);
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
function electronSlotResult(slots, truncated) {
|
|
622
907
|
const malformed = truncated
|
|
623
908
|
|| slots.length > 1
|
|
624
909
|
|| slots.some((slot) => ![0, 1].includes(slot.used) || (slot.used === 1 && slot.version !== 1));
|
|
@@ -631,6 +916,86 @@ export function inspectElectronAsarIntegritySlots(value) {
|
|
|
631
916
|
};
|
|
632
917
|
}
|
|
633
918
|
|
|
919
|
+
export function inspectElectronAsarIntegrityFile(filePath, {
|
|
920
|
+
cache,
|
|
921
|
+
io = fs,
|
|
922
|
+
chunkSize = FILE_READ_CHUNK_BYTES,
|
|
923
|
+
} = {}) {
|
|
924
|
+
if (!Number.isSafeInteger(chunkSize) || chunkSize < 66) {
|
|
925
|
+
throw new TypeError("Electron integrity file chunk size must be at least 66 bytes");
|
|
926
|
+
}
|
|
927
|
+
const realPath = io.realpathSync(filePath);
|
|
928
|
+
const pathStat = io.statSync(realPath, { bigint: true });
|
|
929
|
+
const cacheKey = `electron\0${fileIdentity(realPath, pathStat)}`;
|
|
930
|
+
if (cache?.has(cacheKey)) {
|
|
931
|
+
withVerifiedFile(realPath, {
|
|
932
|
+
expectedIdentity: fileIdentity(realPath, pathStat),
|
|
933
|
+
io,
|
|
934
|
+
label: "executable",
|
|
935
|
+
}, () => {});
|
|
936
|
+
return cache.get(cacheKey);
|
|
937
|
+
}
|
|
938
|
+
const opened = withVerifiedFile(realPath, {
|
|
939
|
+
expectedIdentity: fileIdentity(realPath, pathStat),
|
|
940
|
+
io,
|
|
941
|
+
label: "executable",
|
|
942
|
+
}, ({ file, before }) => {
|
|
943
|
+
const sentinel = Buffer.from(ELECTRON_ASAR_INTEGRITY_SENTINEL);
|
|
944
|
+
const hash = createHash("sha256");
|
|
945
|
+
const chunk = Buffer.allocUnsafe(chunkSize);
|
|
946
|
+
const slots = [];
|
|
947
|
+
let tail = Buffer.alloc(0);
|
|
948
|
+
let position = 0;
|
|
949
|
+
let nextSearchOffset = 0;
|
|
950
|
+
for (;;) {
|
|
951
|
+
const length = io.readSync(file, chunk, 0, chunk.length, position);
|
|
952
|
+
if (length === 0) break;
|
|
953
|
+
const bytes = chunk.subarray(0, length);
|
|
954
|
+
hash.update(bytes);
|
|
955
|
+
const scan = tail.length ? Buffer.concat([tail, bytes]) : bytes;
|
|
956
|
+
const scanOffset = position - tail.length;
|
|
957
|
+
let localOffset = Math.max(0, nextSearchOffset - scanOffset);
|
|
958
|
+
while (localOffset <= scan.length - sentinel.length) {
|
|
959
|
+
const found = scan.indexOf(sentinel, localOffset);
|
|
960
|
+
if (found < 0) break;
|
|
961
|
+
const absoluteOffset = scanOffset + found;
|
|
962
|
+
if (found + 66 > scan.length) break;
|
|
963
|
+
slots.push({
|
|
964
|
+
offset: absoluteOffset,
|
|
965
|
+
used: scan[found + 32],
|
|
966
|
+
version: scan[found + 33],
|
|
967
|
+
digest: scan.subarray(found + 34, found + 66).toString("hex"),
|
|
968
|
+
});
|
|
969
|
+
nextSearchOffset = absoluteOffset + sentinel.length;
|
|
970
|
+
localOffset = nextSearchOffset - scanOffset;
|
|
971
|
+
}
|
|
972
|
+
tail = Buffer.from(scan.subarray(Math.max(0, scan.length - 65)));
|
|
973
|
+
position += length;
|
|
974
|
+
}
|
|
975
|
+
if (position !== Number(before.size)) {
|
|
976
|
+
throw new Error(`PM Studio executable changed while reading: ${filePath}`);
|
|
977
|
+
}
|
|
978
|
+
let truncated = false;
|
|
979
|
+
const tailOffset = position - tail.length;
|
|
980
|
+
let localOffset = Math.max(0, nextSearchOffset - tailOffset);
|
|
981
|
+
while (localOffset <= tail.length - sentinel.length) {
|
|
982
|
+
const found = tail.indexOf(sentinel, localOffset);
|
|
983
|
+
if (found < 0) break;
|
|
984
|
+
if (found + 66 > tail.length) {
|
|
985
|
+
truncated = true;
|
|
986
|
+
break;
|
|
987
|
+
}
|
|
988
|
+
localOffset = found + sentinel.length;
|
|
989
|
+
}
|
|
990
|
+
return {
|
|
991
|
+
sha256: hash.digest("hex"),
|
|
992
|
+
integrity: electronSlotResult(slots, truncated),
|
|
993
|
+
};
|
|
994
|
+
});
|
|
995
|
+
cache?.set(cacheKey, opened.result);
|
|
996
|
+
return opened.result;
|
|
997
|
+
}
|
|
998
|
+
|
|
634
999
|
function sameArray(actual, expected) {
|
|
635
1000
|
return Array.isArray(actual)
|
|
636
1001
|
&& Array.isArray(expected)
|
|
@@ -655,11 +1020,13 @@ function inspectTarget(parsed, target) {
|
|
|
655
1020
|
return { path: target.path, malformed: error.message };
|
|
656
1021
|
}
|
|
657
1022
|
const absoluteOffset = parsed.dataOffset + offset;
|
|
658
|
-
|
|
1023
|
+
const archiveLength = parsed.archiveLength ?? parsed.buffer.length;
|
|
1024
|
+
if (entry.unpacked || absoluteOffset + size > archiveLength) {
|
|
659
1025
|
return { path: target.path, offset, size, absoluteOffset, malformed: "entry data is outside the packed archive" };
|
|
660
1026
|
}
|
|
661
1027
|
|
|
662
|
-
const bytes = parsed.
|
|
1028
|
+
const bytes = parsed.targetBytes?.get(target.path)
|
|
1029
|
+
|| parsed.buffer.subarray(absoluteOffset, absoluteOffset + size);
|
|
663
1030
|
let edit = null;
|
|
664
1031
|
if (target.edit) {
|
|
665
1032
|
let editOffset;
|
|
@@ -740,7 +1107,11 @@ function targetStateIssues(detail, target, state) {
|
|
|
740
1107
|
export function inspectAsarBuffer(value, recipe = PM_STUDIO_2_9_7_RECIPE) {
|
|
741
1108
|
const parsed = parseAsarBuffer(value);
|
|
742
1109
|
const asarSha256 = sha256Hex(parsed.buffer);
|
|
743
|
-
|
|
1110
|
+
return inspectParsedAsar(parsed, asarSha256, recipe);
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1113
|
+
function inspectParsedAsar(parsed, asarSha256, recipe) {
|
|
1114
|
+
const headerSha256 = parsed.headerSha256 || sha256Hex(parsed.headerBytes);
|
|
744
1115
|
const targets = recipe.targets.map((target) => inspectTarget(parsed, target));
|
|
745
1116
|
const commonIssues = [];
|
|
746
1117
|
addMismatch(commonIssues, "ASAR data offset", parsed.dataOffset, recipe.dataOffset);
|
|
@@ -793,6 +1164,15 @@ export function inspectAsarBuffer(value, recipe = PM_STUDIO_2_9_7_RECIPE) {
|
|
|
793
1164
|
};
|
|
794
1165
|
}
|
|
795
1166
|
|
|
1167
|
+
export function inspectAsarFile(filePath, recipe = PM_STUDIO_2_9_7_RECIPE, options = {}) {
|
|
1168
|
+
const evidence = readAsarFileEvidence(filePath, recipe.targets.map((target) => target.path), options);
|
|
1169
|
+
try {
|
|
1170
|
+
return inspectParsedAsar(evidence.parsed, evidence.asarSha256, recipe);
|
|
1171
|
+
} finally {
|
|
1172
|
+
if (options.releaseTargets) evidence.parsed.targetBytes.clear();
|
|
1173
|
+
}
|
|
1174
|
+
}
|
|
1175
|
+
|
|
796
1176
|
export function patchAsarBuffer(value, recipe = PM_STUDIO_2_9_7_RECIPE) {
|
|
797
1177
|
const before = inspectAsarBuffer(value, recipe);
|
|
798
1178
|
if (before.state === "patched") {
|
package/src/pm-studio-relay.mjs
CHANGED
|
@@ -445,11 +445,14 @@ export function createPmStudioRelayHandler(options = {}) {
|
|
|
445
445
|
releaseRequest = releaseOnce(admission);
|
|
446
446
|
abort.setTimeout(requestBodyTimeoutMs, "request_body_timeout");
|
|
447
447
|
const body = await readJsonBody(req, { admission, signal: abort.signal });
|
|
448
|
-
const
|
|
449
|
-
if (
|
|
448
|
+
const routePlan = modelRouter.plan(body?.model);
|
|
449
|
+
if (routePlan.disposition === "reject") {
|
|
450
450
|
throw localError(400, "model_not_supported", "The requested Claude model is not enabled for PM Studio");
|
|
451
451
|
}
|
|
452
|
-
if (
|
|
452
|
+
if (routePlan.disposition !== "relay"
|
|
453
|
+
|| routePlan.origin !== "ccdx"
|
|
454
|
+
|| routePlan.profile !== "claude"
|
|
455
|
+
|| routePlan.protocol !== "openai-chat-completions") {
|
|
453
456
|
throw localError(
|
|
454
457
|
400,
|
|
455
458
|
"native_route_required",
|