codex-copilot-dx 0.6.8 → 0.6.9
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 +20 -9
- package/package.json +1 -1
- package/src/cli-status.mjs +2 -2
- package/src/observability.mjs +0 -4
- package/src/pm-studio-asar.mjs +126 -82
- package/src/pm-studio-relay.mjs +40 -30
- package/src/pm-studio-setup.mjs +103 -36
- package/src/pm-studio-status.mjs +67 -7
- package/src/responses-request.mjs +16 -2
- package/src/running-adapter.mjs +3 -1
package/README.md
CHANGED
|
@@ -112,9 +112,11 @@ ccdx doctor --online --profile all
|
|
|
112
112
|
### PM Studio opt-in patch
|
|
113
113
|
|
|
114
114
|
CCDX can add the isolated Claude catalog to PM Studio without switching its
|
|
115
|
-
active account or profile. GPT requests
|
|
116
|
-
|
|
117
|
-
|
|
115
|
+
active account or profile. GPT requests keep PM Studio's native official GitHub
|
|
116
|
+
Copilot transport and the bearer supplied by the current PM profile. Model
|
|
117
|
+
discovery tries the compatible local CCDX merge first, and only exact enabled
|
|
118
|
+
Anthropic model IDs use the isolated CCDX Claude profile. Neither credential is
|
|
119
|
+
copied into the other application.
|
|
118
120
|
|
|
119
121
|
The current patch recipes support only the exact, unmodified PM Studio 2.9.7
|
|
120
122
|
and 2.9.10 bundles on macOS. This is exact-version support, not a version range:
|
|
@@ -155,12 +157,21 @@ matches the frozen official fingerprint. The staged and installed CCDX patch
|
|
|
155
157
|
must still pass strict ad-hoc signature verification and match the complete
|
|
156
158
|
patched-tree record in the backup manifest.
|
|
157
159
|
|
|
158
|
-
The patch
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
160
|
+
The patch uses split origins, not an App-wide proxy. GPT and other non-Claude
|
|
161
|
+
inference requests remain on PM Studio's native official GitHub Copilot origin;
|
|
162
|
+
they do not enter CCDX or depend on port 2026. Model discovery first tries the
|
|
163
|
+
loopback-only `GET /pm-ccdx/models` merge and accepts it only when the relay
|
|
164
|
+
returns the `X-CCDX-PM-Relay: split-origin-v1` compatibility marker. If the
|
|
165
|
+
relay is unavailable or incompatible, model discovery safely falls back to the
|
|
166
|
+
official directory and GPT remains available. Exact enabled Claude chat uses
|
|
167
|
+
`POST /pm-ccdx/chat/completions` and fails closed if CCDX is unavailable; it
|
|
168
|
+
never falls back to the PM enterprise account. The relay does not expose
|
|
169
|
+
`/responses` or `/embeddings` and is not a general-purpose proxy.
|
|
170
|
+
|
|
171
|
+
The loopback relay trusts local callers that present a valid Copilot bearer.
|
|
172
|
+
CCDX uses the PM bearer only to fetch or validate official model access; it is
|
|
173
|
+
never used as the credential for the isolated Claude upstream request. Keep
|
|
174
|
+
port 2026 local to the machine.
|
|
164
175
|
|
|
165
176
|
Patching replaces the vendor signature with an ad-hoc signature. PM Studio's
|
|
166
177
|
updater is not disabled; an official update may overwrite the patch or may
|
package/package.json
CHANGED
package/src/cli-status.mjs
CHANGED
|
@@ -73,7 +73,7 @@ function routingTarget(value) {
|
|
|
73
73
|
function pmRouteSummary(requests = {}) {
|
|
74
74
|
const routes = requests.by_route;
|
|
75
75
|
if (!routes || typeof routes !== "object") return null;
|
|
76
|
-
const names = ["pm_models", "pm_chat_completions"
|
|
76
|
+
const names = ["pm_models", "pm_chat_completions"];
|
|
77
77
|
const totals = names.reduce((summary, name) => {
|
|
78
78
|
const route = routes[name] || {};
|
|
79
79
|
summary.total += finiteNumber(route.total) || 0;
|
|
@@ -81,7 +81,7 @@ function pmRouteSummary(requests = {}) {
|
|
|
81
81
|
summary.errors += finiteNumber(route.errors) || 0;
|
|
82
82
|
return summary;
|
|
83
83
|
}, { total: 0, active: 0, errors: 0 });
|
|
84
|
-
return `PM relay: ${count(totals.total)} requests, ${count(totals.active)} active, ${count(totals.errors)} errors; models ${count(routes.pm_models?.total)}, chat ${count(routes.pm_chat_completions?.total)}
|
|
84
|
+
return `PM relay: ${count(totals.total)} requests, ${count(totals.active)} active, ${count(totals.errors)} errors; models ${count(routes.pm_models?.total)}, chat ${count(routes.pm_chat_completions?.total)}`;
|
|
85
85
|
}
|
|
86
86
|
|
|
87
87
|
function errorCode(error) {
|
package/src/observability.mjs
CHANGED
|
@@ -17,8 +17,6 @@ const ROUTE_NAMES = Object.freeze([
|
|
|
17
17
|
"messages_count_tokens",
|
|
18
18
|
"pm_models",
|
|
19
19
|
"pm_chat_completions",
|
|
20
|
-
"pm_responses",
|
|
21
|
-
"pm_embeddings",
|
|
22
20
|
"not_found",
|
|
23
21
|
]);
|
|
24
22
|
|
|
@@ -104,8 +102,6 @@ export function classifyAdapterRoute(method, pathname) {
|
|
|
104
102
|
if (method === "POST" && pathname === "/v1/messages/count_tokens") return "messages_count_tokens";
|
|
105
103
|
if (method === "GET" && pathname === "/pm-ccdx/models") return "pm_models";
|
|
106
104
|
if (method === "POST" && pathname === "/pm-ccdx/chat/completions") return "pm_chat_completions";
|
|
107
|
-
if (method === "POST" && pathname === "/pm-ccdx/responses") return "pm_responses";
|
|
108
|
-
if (method === "POST" && pathname === "/pm-ccdx/embeddings") return "pm_embeddings";
|
|
109
105
|
return "not_found";
|
|
110
106
|
}
|
|
111
107
|
|
package/src/pm-studio-asar.mjs
CHANGED
|
@@ -2,8 +2,22 @@ import { createHash } from "node:crypto";
|
|
|
2
2
|
|
|
3
3
|
export const PM_STUDIO_ORIGIN = "https://api.githubcopilot.com";
|
|
4
4
|
export const CCDX_PM_STUDIO_ORIGIN = "http://127.0.0.1:2026/pm-ccdx";
|
|
5
|
+
export const PM_STUDIO_SPLIT_ORIGIN_MARKER = "split-origin-v1";
|
|
5
6
|
export const ELECTRON_ASAR_INTEGRITY_SENTINEL = "AGbevlPCksUGKNL8TSn7wGmJEuJsXb2A";
|
|
6
7
|
|
|
8
|
+
const PM_STUDIO_COPILOT_CONFIG_MODULE_LENGTH = 2_121;
|
|
9
|
+
const PM_STUDIO_COPILOT_CONFIG_MODULE_SOURCE_SHA256 = "3da6245d676015ab6cd26e4d6a0d2f54894a687c370aa419c5ad170533d086aa";
|
|
10
|
+
|
|
11
|
+
function splitOriginConfigModule() {
|
|
12
|
+
const source = `47024(I,e,t){"use strict";t.d(e,{qn:()=>l});const l={CLIENT_ID:"Iv1.b507a08c87ecfe98",CLIENT_SECRET:void 0,API_ENDPOINT:"${PM_STUDIO_ORIGIN}",DEVICE_CODE_URL:"https://github.com/login/device/code",ACCESS_TOKEN_URL:"https://github.com/login/oauth/access_token",COPILOT_TOKEN_URL:"https://api.github.com/copilot_internal/v2/token",USER_AGENT:"GitHubCopilotChat/0.26.7",EDITOR_VERSION:"vscode/1.99.3",EDITOR_PLUGIN_VERSION:"copilot-chat/0.26.7",INTEGRATION_ID:"vscode-chat",STANDARD_HEADERS:{Accept:"application/json","Content-Type":"application/json","User-Agent":"GitHubCopilotChat/0.26.7","Editor-Version":"vscode/1.99.3","Editor-Plugin-Version":"copilot-chat/0.26.7","Copilot-Integration-Id":"vscode-chat","X-Request-Id":()=>\`req_\${Date.now()}_\${Math.random().toString(36).substr(2,9)}\`}};const c=fetch.bind(globalThis),n="${CCDX_PM_STUDIO_ORIGIN}",a="${PM_STUDIO_SPLIT_ORIGIN_MARKER}",h="X-CCDX-PM-Relay",d=new Set,m=I=>I.headers.get(h)===a,q=I=>AbortSignal.any([I,AbortSignal.timeout(750)].filter(Boolean)),f=(I,e=400)=>Response.json({error:{code:I}},{status:e,headers:{[h]:a}});globalThis.fetch=async(I,e)=>{const t=I.url||I+"";if(t===\`\${l.API_ENDPOINT}/models\`){try{const t=await c(\`\${n}/models\`,{...e,signal:q(e?.signal)});if(t.ok&&m(t))try{let I=await t.clone().json(),e=I.data||I;if(Array.isArray(e)){d.clear();for(const I of e)/anthropic/i.test(I.vendor||I.owned_by)&&d.add(I.id);return t}}catch{}t.body?.cancel?.()?.catch?.(()=>{})}catch(I){if(e?.signal?.aborted)throw e.signal.reason||I}return c(I,e)}const s=t.startsWith(l.API_ENDPOINT)?t.slice(l.API_ENDPOINT.length):"";if("POST"===e?.method&&["/chat/completions","/responses","/embeddings"].includes(s)){let t="";try{t=(JSON.parse(e.body).model||"").trim()}catch{}if(d.has(t)||/^claude-/i.test(t)){if("/chat/completions"!==s)return f("model_not_supported");try{const I=await c(\`\${n}/models\`,{signal:q(e?.signal)}),t=401===I.status&&m(I);await I.text();if(!t)throw 0;const l=await c(\`\${n}\${s}\`,e);if(!m(l)){await l.body?.cancel?.();throw 0}return l}catch(I){if(e?.signal?.aborted)throw e.signal.reason||I;return f("relay_incompatible",503)}}}return c(I,e)}}`;
|
|
13
|
+
if (source.length > PM_STUDIO_COPILOT_CONFIG_MODULE_LENGTH) {
|
|
14
|
+
throw new Error("PM Studio split-origin module exceeds its exact source window");
|
|
15
|
+
}
|
|
16
|
+
return `${source.slice(0, -1)}${" ".repeat(PM_STUDIO_COPILOT_CONFIG_MODULE_LENGTH - source.length)}}`;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export const PM_STUDIO_SPLIT_ORIGIN_CONFIG_MODULE = splitOriginConfigModule();
|
|
20
|
+
|
|
7
21
|
export const PM_STUDIO_2_9_7_RECIPE = Object.freeze({
|
|
8
22
|
id: "pm-studio-2.9.7-build-2.9.7",
|
|
9
23
|
version: "2.9.7",
|
|
@@ -21,50 +35,43 @@ export const PM_STUDIO_2_9_7_RECIPE = Object.freeze({
|
|
|
21
35
|
dataOffset: 3_832_764,
|
|
22
36
|
sourceAsarSha256: "36fb8bf2fc9326ddf49baab5cc0b54b38751a7db041bda3c42a3b01107b54118",
|
|
23
37
|
sourceHeaderSha256: "87746d92e00db2d40c03830ea06de22ec7cba112d07729801ccd02e226ffd961",
|
|
24
|
-
patchedAsarSha256: "
|
|
25
|
-
patchedHeaderSha256: "
|
|
26
|
-
sourceSentinel: PM_STUDIO_ORIGIN,
|
|
27
|
-
patchedSentinel: CCDX_PM_STUDIO_ORIGIN,
|
|
38
|
+
patchedAsarSha256: "d520d115604225c1a3feb749dbe29ad0d2cd175c5233c010de0e1fade527fa0b",
|
|
39
|
+
patchedHeaderSha256: "83e5fba3b6751b3098d8f6901db361a9b620ab3b924e0e551d3df612ff412642",
|
|
28
40
|
targets: Object.freeze([
|
|
29
41
|
Object.freeze({
|
|
30
42
|
path: "dist/main/main.js",
|
|
31
43
|
offset: 101_621_627,
|
|
32
44
|
size: 9_909_974,
|
|
33
|
-
sentinelOffset: 1_120_480,
|
|
34
|
-
absoluteSentinelOffset: 106_574_871,
|
|
35
|
-
sentinelCount: 1,
|
|
36
45
|
blockSize: 4_194_304,
|
|
37
46
|
sourceSha256: "58383be112886becdb8d8e7cce1b3efce07be6716190aa4940627f456eb8edb2",
|
|
38
|
-
patchedSha256: "
|
|
47
|
+
patchedSha256: "69156040fb233a835b7be2f86fbe2f653380f03379adfedf46c63ab45de9d39d",
|
|
39
48
|
sourceBlocks: Object.freeze([
|
|
40
49
|
"3ce05a21e8992ccf12966cf9b78fcd28b2f66e6786489b5bb0a06f1d4b2a0c1e",
|
|
41
50
|
"0a4832413d8d8cdda67c8bb318c0e5c25df20c75802d637097ffd045a31cd443",
|
|
42
51
|
"bcc10582d8a74717f8f81b98182d3551f1eaf23eb9cadd908d160ce5e4ef8cba",
|
|
43
52
|
]),
|
|
44
53
|
patchedBlocks: Object.freeze([
|
|
45
|
-
"
|
|
54
|
+
"f0b5585d2ff0d403e5c4d35e9bd0b8cbfec5c41a9b9057b8cc808107e889cf46",
|
|
46
55
|
"0a4832413d8d8cdda67c8bb318c0e5c25df20c75802d637097ffd045a31cd443",
|
|
47
56
|
"bcc10582d8a74717f8f81b98182d3551f1eaf23eb9cadd908d160ce5e4ef8cba",
|
|
48
57
|
]),
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
patchedSha256: "89bbea9516a914da3fa4f32befd33d0dbb0e63b09545e4d85392bccb52adc145",
|
|
60
|
-
sourceBlocks: Object.freeze([
|
|
61
|
-
"eb63abf35be3db101da179789e8ffdcc5b67131258ac8d7976109713ff0cc994",
|
|
62
|
-
]),
|
|
63
|
-
patchedBlocks: Object.freeze([
|
|
64
|
-
"89bbea9516a914da3fa4f32befd33d0dbb0e63b09545e4d85392bccb52adc145",
|
|
65
|
-
]),
|
|
58
|
+
edit: Object.freeze({
|
|
59
|
+
offset: 1_120_359,
|
|
60
|
+
absoluteOffset: 106_574_750,
|
|
61
|
+
length: PM_STUDIO_COPILOT_CONFIG_MODULE_LENGTH,
|
|
62
|
+
anchor: "47024(I,e,t){",
|
|
63
|
+
anchorCount: 1,
|
|
64
|
+
sourceSha256: PM_STUDIO_COPILOT_CONFIG_MODULE_SOURCE_SHA256,
|
|
65
|
+
patchedSha256: "a5b9312ed78e8a7fc3bd38f7ea5d4ab008f21bae18ba3dd6aa2d4be6b69191db",
|
|
66
|
+
replacement: PM_STUDIO_SPLIT_ORIGIN_CONFIG_MODULE,
|
|
67
|
+
}),
|
|
66
68
|
}),
|
|
67
69
|
]),
|
|
70
|
+
legacy: Object.freeze({
|
|
71
|
+
kind: "global-origin-replacement",
|
|
72
|
+
asarSha256: "3dd0f53cdaa35a644d2cf56e4fc2dd20f5c90dc2989b6d81a467ef00ebb620a7",
|
|
73
|
+
headerSha256: "86ed1113a563b82ecec05de89aab87a5fd1666f35bee76e2d76e2ed5f6dcb3fa",
|
|
74
|
+
}),
|
|
68
75
|
});
|
|
69
76
|
|
|
70
77
|
export const PM_STUDIO_2_9_10_RECIPE = Object.freeze({
|
|
@@ -117,50 +124,43 @@ export const PM_STUDIO_2_9_10_RECIPE = Object.freeze({
|
|
|
117
124
|
dataOffset: 3_833_292,
|
|
118
125
|
sourceAsarSha256: "d243860770e8b1d8044213924f9704d1fc52f900d6c33461eff6358962330b78",
|
|
119
126
|
sourceHeaderSha256: "5fc77d5b61e9647fba10fd2bd5752d0889c14da5a00b21d31bc48fdcccf74a69",
|
|
120
|
-
patchedAsarSha256: "
|
|
121
|
-
patchedHeaderSha256: "
|
|
122
|
-
sourceSentinel: PM_STUDIO_ORIGIN,
|
|
123
|
-
patchedSentinel: CCDX_PM_STUDIO_ORIGIN,
|
|
127
|
+
patchedAsarSha256: "ea28d998056b32ca4115d208a4d81ce629c83f3f5f89c6a66d50749849beb6fc",
|
|
128
|
+
patchedHeaderSha256: "348d80f5f2d1ebb92e6089195b1703b87467678f80d8946b8ab83b3ead03e5be",
|
|
124
129
|
targets: Object.freeze([
|
|
125
130
|
Object.freeze({
|
|
126
131
|
path: "dist/main/main.js",
|
|
127
132
|
offset: 101_622_115,
|
|
128
133
|
size: 10_150_157,
|
|
129
|
-
sentinelOffset: 1_241_344,
|
|
130
|
-
absoluteSentinelOffset: 106_696_751,
|
|
131
|
-
sentinelCount: 1,
|
|
132
134
|
blockSize: 4_194_304,
|
|
133
135
|
sourceSha256: "03607bb1edc068396b99f02b9c9e0d8e10388fb658afc112e06e875c123dbc27",
|
|
134
|
-
patchedSha256: "
|
|
136
|
+
patchedSha256: "b5b5e37af3d06ffc603bdef620fc1e0254580f0ee325f1a77096af6c78379b79",
|
|
135
137
|
sourceBlocks: Object.freeze([
|
|
136
138
|
"3910a9bc5459f493956aafee7b3644e8d242027758f0dcdea97d7c10a324c6bf",
|
|
137
139
|
"49fa64c68425d35b1baff0d5745d856d377e014a7a7829322cd1b39aade9532e",
|
|
138
140
|
"bd7cc14a1d2e02abe66c0ec6fc4e97b263cfc1f67c4ae39e956c06d3bcc2fe67",
|
|
139
141
|
]),
|
|
140
142
|
patchedBlocks: Object.freeze([
|
|
141
|
-
"
|
|
143
|
+
"63aa15d8c44614a7b2d9656ddba23109d40bab66b66b36339154953bd164da75",
|
|
142
144
|
"49fa64c68425d35b1baff0d5745d856d377e014a7a7829322cd1b39aade9532e",
|
|
143
145
|
"bd7cc14a1d2e02abe66c0ec6fc4e97b263cfc1f67c4ae39e956c06d3bcc2fe67",
|
|
144
146
|
]),
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
patchedSha256: "dc4a9d550f4beb3a2cd7ad0fa6fc82286106762bdf789189d8f88abd0a90f619",
|
|
156
|
-
sourceBlocks: Object.freeze([
|
|
157
|
-
"3d123f7a3eb91777f723fe2c4d412677e6722b6348373e4b4bfda41dda8b60ad",
|
|
158
|
-
]),
|
|
159
|
-
patchedBlocks: Object.freeze([
|
|
160
|
-
"dc4a9d550f4beb3a2cd7ad0fa6fc82286106762bdf789189d8f88abd0a90f619",
|
|
161
|
-
]),
|
|
147
|
+
edit: Object.freeze({
|
|
148
|
+
offset: 1_241_223,
|
|
149
|
+
absoluteOffset: 106_696_630,
|
|
150
|
+
length: PM_STUDIO_COPILOT_CONFIG_MODULE_LENGTH,
|
|
151
|
+
anchor: "47024(I,e,t){",
|
|
152
|
+
anchorCount: 1,
|
|
153
|
+
sourceSha256: PM_STUDIO_COPILOT_CONFIG_MODULE_SOURCE_SHA256,
|
|
154
|
+
patchedSha256: "a5b9312ed78e8a7fc3bd38f7ea5d4ab008f21bae18ba3dd6aa2d4be6b69191db",
|
|
155
|
+
replacement: PM_STUDIO_SPLIT_ORIGIN_CONFIG_MODULE,
|
|
156
|
+
}),
|
|
162
157
|
}),
|
|
163
158
|
]),
|
|
159
|
+
legacy: Object.freeze({
|
|
160
|
+
kind: "global-origin-replacement",
|
|
161
|
+
asarSha256: "49f72d999a6085102341c2d551577e164db6f22e4707d2112efa3fd280e7315e",
|
|
162
|
+
headerSha256: "b264ece5b8e469cd3885a7a91f904f1902e1991f214ecbf228dfe91059ac7008",
|
|
163
|
+
}),
|
|
164
164
|
});
|
|
165
165
|
|
|
166
166
|
export const PM_STUDIO_RECIPES = Object.freeze([
|
|
@@ -334,7 +334,7 @@ function addMismatch(issues, label, actual, expected) {
|
|
|
334
334
|
if (actual !== expected) issues.push(`${label}: expected ${expected}, got ${actual}`);
|
|
335
335
|
}
|
|
336
336
|
|
|
337
|
-
function inspectTarget(parsed, target
|
|
337
|
+
function inspectTarget(parsed, target) {
|
|
338
338
|
const entry = findAsarEntry(parsed.header, target.path);
|
|
339
339
|
if (!entry) return { path: target.path, missing: true };
|
|
340
340
|
|
|
@@ -352,8 +352,28 @@ function inspectTarget(parsed, target, recipe) {
|
|
|
352
352
|
}
|
|
353
353
|
|
|
354
354
|
const bytes = parsed.buffer.subarray(absoluteOffset, absoluteOffset + size);
|
|
355
|
-
|
|
356
|
-
|
|
355
|
+
let edit = null;
|
|
356
|
+
if (target.edit) {
|
|
357
|
+
let editOffset;
|
|
358
|
+
let editLength;
|
|
359
|
+
try {
|
|
360
|
+
editOffset = integer(target.edit.offset, `${target.path} edit offset`);
|
|
361
|
+
editLength = integer(target.edit.length, `${target.path} edit length`);
|
|
362
|
+
} catch (error) {
|
|
363
|
+
return { path: target.path, offset, size, absoluteOffset, malformed: error.message };
|
|
364
|
+
}
|
|
365
|
+
if (editLength === 0 || editOffset + editLength > bytes.length) {
|
|
366
|
+
return { path: target.path, offset, size, absoluteOffset, malformed: "edit window is outside the target" };
|
|
367
|
+
}
|
|
368
|
+
const anchor = Buffer.from(target.edit.anchor || "");
|
|
369
|
+
edit = {
|
|
370
|
+
offset: editOffset,
|
|
371
|
+
absoluteOffset: absoluteOffset + editOffset,
|
|
372
|
+
length: editLength,
|
|
373
|
+
sha256: sha256Hex(bytes.subarray(editOffset, editOffset + editLength)),
|
|
374
|
+
anchorPositions: occurrencePositions(bytes, anchor),
|
|
375
|
+
};
|
|
376
|
+
}
|
|
357
377
|
return {
|
|
358
378
|
path: target.path,
|
|
359
379
|
offset,
|
|
@@ -363,20 +383,17 @@ function inspectTarget(parsed, target, recipe) {
|
|
|
363
383
|
blockSize: entry.integrity?.blockSize,
|
|
364
384
|
blocks: blockSha256(bytes, target.blockSize),
|
|
365
385
|
integrity: entry.integrity || null,
|
|
366
|
-
|
|
367
|
-
patchedSentinelPositions: occurrencePositions(bytes, patchedSentinel),
|
|
386
|
+
edit,
|
|
368
387
|
};
|
|
369
388
|
}
|
|
370
389
|
|
|
371
|
-
function targetStateIssues(detail, target,
|
|
390
|
+
function targetStateIssues(detail, target, state) {
|
|
372
391
|
const issues = [];
|
|
373
392
|
if (detail.missing) return [`${target.path}: entry is missing`];
|
|
374
393
|
if (detail.malformed) return [`${target.path}: ${detail.malformed}`];
|
|
375
394
|
|
|
376
395
|
addMismatch(issues, `${target.path} offset`, detail.offset, target.offset);
|
|
377
396
|
addMismatch(issues, `${target.path} size`, detail.size, target.size);
|
|
378
|
-
addMismatch(issues, `${target.path} absolute sentinel offset`,
|
|
379
|
-
detail.absoluteOffset + target.sentinelOffset, target.absoluteSentinelOffset);
|
|
380
397
|
addMismatch(issues, `${target.path} integrity algorithm`,
|
|
381
398
|
detail.integrity?.algorithm, "SHA256");
|
|
382
399
|
addMismatch(issues, `${target.path} block size`, detail.integrity?.blockSize, target.blockSize);
|
|
@@ -384,20 +401,23 @@ function targetStateIssues(detail, target, recipe, state) {
|
|
|
384
401
|
const patched = state === "patched";
|
|
385
402
|
const expectedHash = patched ? target.patchedSha256 : target.sourceSha256;
|
|
386
403
|
const expectedBlocks = patched ? target.patchedBlocks : target.sourceBlocks;
|
|
387
|
-
const expectedSentinelPositions = [target.sentinelOffset];
|
|
388
|
-
const presentPositions = patched ? detail.patchedSentinelPositions : detail.sourceSentinelPositions;
|
|
389
|
-
const absentPositions = patched ? detail.sourceSentinelPositions : detail.patchedSentinelPositions;
|
|
390
404
|
|
|
391
405
|
addMismatch(issues, `${target.path} SHA-256`, detail.sha256, expectedHash);
|
|
392
406
|
addMismatch(issues, `${target.path} integrity hash`, detail.integrity?.hash, expectedHash);
|
|
393
407
|
if (!sameArray(detail.blocks, expectedBlocks)) issues.push(`${target.path} computed block hashes do not match ${state} recipe`);
|
|
394
408
|
if (!sameArray(detail.integrity?.blocks, expectedBlocks)) issues.push(`${target.path} header block hashes do not match ${state} recipe`);
|
|
395
|
-
if (
|
|
396
|
-
issues
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
409
|
+
if (target.edit) {
|
|
410
|
+
addMismatch(issues, `${target.path} edit offset`, detail.edit?.offset, target.edit.offset);
|
|
411
|
+
addMismatch(issues, `${target.path} absolute edit offset`, detail.edit?.absoluteOffset, target.edit.absoluteOffset);
|
|
412
|
+
addMismatch(issues, `${target.path} edit length`, detail.edit?.length, target.edit.length);
|
|
413
|
+
addMismatch(issues, `${target.path} edit SHA-256`, detail.edit?.sha256,
|
|
414
|
+
patched ? target.edit.patchedSha256 : target.edit.sourceSha256);
|
|
415
|
+
if (!sameArray(detail.edit?.anchorPositions, [target.edit.offset])) {
|
|
416
|
+
issues.push(`${target.path} module anchor position/count does not match recipe`);
|
|
417
|
+
}
|
|
418
|
+
if (detail.edit?.anchorPositions?.length !== target.edit.anchorCount) {
|
|
419
|
+
issues.push(`${target.path} module anchor count is ${detail.edit?.anchorPositions?.length || 0}, expected ${target.edit.anchorCount}`);
|
|
420
|
+
}
|
|
401
421
|
}
|
|
402
422
|
return issues;
|
|
403
423
|
}
|
|
@@ -406,7 +426,7 @@ export function inspectAsarBuffer(value, recipe = PM_STUDIO_2_9_7_RECIPE) {
|
|
|
406
426
|
const parsed = parseAsarBuffer(value);
|
|
407
427
|
const asarSha256 = sha256Hex(parsed.buffer);
|
|
408
428
|
const headerSha256 = sha256Hex(parsed.headerBytes);
|
|
409
|
-
const targets = recipe.targets.map((target) => inspectTarget(parsed, target
|
|
429
|
+
const targets = recipe.targets.map((target) => inspectTarget(parsed, target));
|
|
410
430
|
const commonIssues = [];
|
|
411
431
|
addMismatch(commonIssues, "ASAR data offset", parsed.dataOffset, recipe.dataOffset);
|
|
412
432
|
|
|
@@ -418,14 +438,25 @@ export function inspectAsarBuffer(value, recipe = PM_STUDIO_2_9_7_RECIPE) {
|
|
|
418
438
|
addMismatch(issues, "ASAR header SHA-256", headerSha256,
|
|
419
439
|
patched ? recipe.patchedHeaderSha256 : recipe.sourceHeaderSha256);
|
|
420
440
|
for (let index = 0; index < recipe.targets.length; index += 1) {
|
|
421
|
-
issues.push(...targetStateIssues(targets[index], recipe.targets[index],
|
|
441
|
+
issues.push(...targetStateIssues(targets[index], recipe.targets[index], state));
|
|
422
442
|
}
|
|
423
443
|
return issues;
|
|
424
444
|
};
|
|
425
445
|
|
|
426
446
|
const cleanIssues = issuesFor("clean");
|
|
427
447
|
const patchedIssues = issuesFor("patched");
|
|
428
|
-
const
|
|
448
|
+
const legacyIssues = [...commonIssues];
|
|
449
|
+
if (!recipe.legacy) {
|
|
450
|
+
legacyIssues.push("recipe has no recognized legacy patch");
|
|
451
|
+
} else {
|
|
452
|
+
addMismatch(legacyIssues, "ASAR SHA-256", asarSha256, recipe.legacy.asarSha256);
|
|
453
|
+
addMismatch(legacyIssues, "ASAR header SHA-256", headerSha256, recipe.legacy.headerSha256);
|
|
454
|
+
}
|
|
455
|
+
const state = cleanIssues.length === 0
|
|
456
|
+
? "clean"
|
|
457
|
+
: patchedIssues.length === 0
|
|
458
|
+
? "patched"
|
|
459
|
+
: legacyIssues.length === 0 ? "legacy" : "drift";
|
|
429
460
|
return {
|
|
430
461
|
recipeId: recipe.id,
|
|
431
462
|
state,
|
|
@@ -435,19 +466,23 @@ export function inspectAsarBuffer(value, recipe = PM_STUDIO_2_9_7_RECIPE) {
|
|
|
435
466
|
targets,
|
|
436
467
|
cleanIssues,
|
|
437
468
|
patchedIssues,
|
|
469
|
+
legacyIssues,
|
|
438
470
|
};
|
|
439
471
|
}
|
|
440
472
|
|
|
441
473
|
export function patchAsarBuffer(value, recipe = PM_STUDIO_2_9_7_RECIPE) {
|
|
442
|
-
if (Buffer.byteLength(recipe.sourceSentinel) !== Buffer.byteLength(recipe.patchedSentinel)) {
|
|
443
|
-
throw new Error("PM Studio patch requires equal-length origin sentinels");
|
|
444
|
-
}
|
|
445
474
|
const before = inspectAsarBuffer(value, recipe);
|
|
446
475
|
if (before.state === "patched") {
|
|
447
476
|
return { changed: false, buffer: Buffer.from(value), before, after: before };
|
|
448
477
|
}
|
|
478
|
+
if (before.state === "legacy") {
|
|
479
|
+
const error = new Error("Legacy PM Studio global-origin patch must be migrated from a verified clean backup");
|
|
480
|
+
error.code = "PM_STUDIO_ASAR_LEGACY";
|
|
481
|
+
error.inspection = before;
|
|
482
|
+
throw error;
|
|
483
|
+
}
|
|
449
484
|
if (before.state !== "clean") {
|
|
450
|
-
const error = new Error("PM Studio ASAR does not match the clean or
|
|
485
|
+
const error = new Error("PM Studio ASAR does not match the clean, split-origin, or legacy recipe");
|
|
451
486
|
error.code = "PM_STUDIO_ASAR_DRIFT";
|
|
452
487
|
error.inspection = before;
|
|
453
488
|
throw error;
|
|
@@ -455,16 +490,25 @@ export function patchAsarBuffer(value, recipe = PM_STUDIO_2_9_7_RECIPE) {
|
|
|
455
490
|
|
|
456
491
|
const output = Buffer.from(value);
|
|
457
492
|
const parsed = parseAsarBuffer(output);
|
|
458
|
-
const sourceSentinel = Buffer.from(recipe.sourceSentinel);
|
|
459
|
-
const patchedSentinel = Buffer.from(recipe.patchedSentinel);
|
|
460
493
|
|
|
461
494
|
for (const target of recipe.targets) {
|
|
462
495
|
const entry = findAsarEntry(parsed.header, target.path);
|
|
463
496
|
const offset = entryOffset(entry.offset, `${target.path} offset`);
|
|
464
|
-
const
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
497
|
+
const edit = target.edit;
|
|
498
|
+
if (!edit) throw new Error(`${target.path} has no split-origin edit recipe`);
|
|
499
|
+
const replacement = Buffer.from(edit.replacement);
|
|
500
|
+
if (replacement.length !== edit.length || sha256Hex(replacement) !== edit.patchedSha256) {
|
|
501
|
+
throw new Error(`${target.path} split-origin replacement does not match its recipe`);
|
|
502
|
+
}
|
|
503
|
+
const absoluteEditOffset = parsed.dataOffset + offset + edit.offset;
|
|
504
|
+
if (absoluteEditOffset !== edit.absoluteOffset) {
|
|
505
|
+
throw new Error(`${target.path} split-origin edit moved before patching`);
|
|
506
|
+
}
|
|
507
|
+
const existing = output.subarray(absoluteEditOffset, absoluteEditOffset + edit.length);
|
|
508
|
+
if (sha256Hex(existing) !== edit.sourceSha256) {
|
|
509
|
+
throw new Error(`${target.path} source edit window does not match its recipe`);
|
|
510
|
+
}
|
|
511
|
+
replacement.copy(output, absoluteEditOffset);
|
|
468
512
|
entry.integrity = {
|
|
469
513
|
...entry.integrity,
|
|
470
514
|
algorithm: "SHA256",
|
package/src/pm-studio-relay.mjs
CHANGED
|
@@ -16,6 +16,8 @@ import { requireUpstreamEventStream } from "./stream-contract.mjs";
|
|
|
16
16
|
import { safeUpstreamResponseHeaders } from "./upstream-headers.mjs";
|
|
17
17
|
|
|
18
18
|
export const PM_STUDIO_RELAY_PREFIX = "/pm-ccdx";
|
|
19
|
+
export const PM_STUDIO_RELAY_MARKER_HEADER = "X-CCDX-PM-Relay";
|
|
20
|
+
export const PM_STUDIO_RELAY_MARKER_VALUE = "split-origin-v1";
|
|
19
21
|
const COPILOT_ORIGIN = "https://api.githubcopilot.com";
|
|
20
22
|
const DEFAULT_VALIDATION_TTL_MS = 5 * 60 * 1000;
|
|
21
23
|
const DEFAULT_VALIDATION_CACHE_SIZE = 128;
|
|
@@ -32,11 +34,7 @@ const HOP_BY_HOP_HEADERS = new Set([
|
|
|
32
34
|
"transfer-encoding",
|
|
33
35
|
"upgrade",
|
|
34
36
|
]);
|
|
35
|
-
const
|
|
36
|
-
"/chat/completions",
|
|
37
|
-
"/responses",
|
|
38
|
-
"/embeddings",
|
|
39
|
-
]);
|
|
37
|
+
const CHAT_COMPLETIONS_PATH = "/chat/completions";
|
|
40
38
|
|
|
41
39
|
function localError(statusCode, code, message) {
|
|
42
40
|
const error = httpError(message, statusCode);
|
|
@@ -46,7 +44,11 @@ function localError(statusCode, code, message) {
|
|
|
46
44
|
|
|
47
45
|
function sendLocalError(res, statusCode, code, message, headers = {}) {
|
|
48
46
|
if (res.destroyed || res.writableEnded) return;
|
|
49
|
-
res.writeHead(statusCode, {
|
|
47
|
+
res.writeHead(statusCode, {
|
|
48
|
+
"Content-Type": "application/json",
|
|
49
|
+
[PM_STUDIO_RELAY_MARKER_HEADER]: PM_STUDIO_RELAY_MARKER_VALUE,
|
|
50
|
+
...headers,
|
|
51
|
+
});
|
|
50
52
|
res.end(JSON.stringify({ error: { message, type: "invalid_request_error", code } }));
|
|
51
53
|
}
|
|
52
54
|
|
|
@@ -160,6 +162,7 @@ async function responseSnapshot(response) {
|
|
|
160
162
|
function responseHeaders(headers, body, options) {
|
|
161
163
|
return {
|
|
162
164
|
...safeUpstreamResponseHeaders(headers, options),
|
|
165
|
+
[PM_STUDIO_RELAY_MARKER_HEADER]: PM_STUDIO_RELAY_MARKER_VALUE,
|
|
163
166
|
"Content-Length": String(body.length),
|
|
164
167
|
};
|
|
165
168
|
}
|
|
@@ -179,8 +182,6 @@ function rejectUpstreamRedirect(response) {
|
|
|
179
182
|
|
|
180
183
|
function mergeModelCatalog(snapshot, modelRouter) {
|
|
181
184
|
if (!snapshot.ok) return null;
|
|
182
|
-
const allowed = modelRouter.allowedModels();
|
|
183
|
-
if (!allowed.length) return null;
|
|
184
185
|
let enterprise;
|
|
185
186
|
try {
|
|
186
187
|
enterprise = JSON.parse(snapshot.body.toString("utf8"));
|
|
@@ -189,6 +190,8 @@ function mergeModelCatalog(snapshot, modelRouter) {
|
|
|
189
190
|
}
|
|
190
191
|
const data = catalogData(enterprise);
|
|
191
192
|
if (!data) return null;
|
|
193
|
+
const allowed = modelRouter.allowedModels();
|
|
194
|
+
if (!allowed.length) return snapshot.body;
|
|
192
195
|
const seen = new Set(data.map((model) => String(model?.id || "").trim()).filter(Boolean));
|
|
193
196
|
const additions = [];
|
|
194
197
|
for (const model of allowed) {
|
|
@@ -197,7 +200,7 @@ function mergeModelCatalog(snapshot, modelRouter) {
|
|
|
197
200
|
seen.add(id);
|
|
198
201
|
additions.push(model);
|
|
199
202
|
}
|
|
200
|
-
if (!additions.length) return
|
|
203
|
+
if (!additions.length) return snapshot.body;
|
|
201
204
|
return Buffer.from(JSON.stringify(Array.isArray(enterprise)
|
|
202
205
|
? [...enterprise, ...additions]
|
|
203
206
|
: { ...enterprise, data: [...data, ...additions] }));
|
|
@@ -215,6 +218,7 @@ async function relayUpstreamResponse(response, reqBody, res, abort, streamIdleTi
|
|
|
215
218
|
abort.setTimeout(streamIdleTimeoutMs, "stream_idle_timeout");
|
|
216
219
|
res.writeHead(response.status, {
|
|
217
220
|
...safeUpstreamResponseHeaders(response.headers, { contentType: "text/event-stream" }),
|
|
221
|
+
[PM_STUDIO_RELAY_MARKER_HEADER]: PM_STUDIO_RELAY_MARKER_VALUE,
|
|
218
222
|
"Cache-Control": "no-cache",
|
|
219
223
|
});
|
|
220
224
|
const reader = response.body.getReader();
|
|
@@ -424,6 +428,9 @@ export function createPmStudioRelayHandler(options = {}) {
|
|
|
424
428
|
}
|
|
425
429
|
const merged = mergeModelCatalog(snapshot, modelRouter);
|
|
426
430
|
if (!merged) {
|
|
431
|
+
throw localError(502, "invalid_upstream", "PM Studio upstream model catalog has an invalid JSON shape");
|
|
432
|
+
}
|
|
433
|
+
if (merged === snapshot.body) {
|
|
427
434
|
sendSnapshot(res, snapshot);
|
|
428
435
|
return;
|
|
429
436
|
}
|
|
@@ -431,15 +438,26 @@ export function createPmStudioRelayHandler(options = {}) {
|
|
|
431
438
|
res.end(merged);
|
|
432
439
|
}
|
|
433
440
|
|
|
434
|
-
async function
|
|
441
|
+
async function handleChatCompletions(req, res, token, abort) {
|
|
435
442
|
let releaseRequest = releaseOnce();
|
|
436
443
|
try {
|
|
437
444
|
const admission = await acquireRequest(req, { signal: abort.signal });
|
|
438
445
|
releaseRequest = releaseOnce(admission);
|
|
439
446
|
abort.setTimeout(requestBodyTimeoutMs, "request_body_timeout");
|
|
440
447
|
const body = await readJsonBody(req, { admission, signal: abort.signal });
|
|
441
|
-
const
|
|
442
|
-
|
|
448
|
+
const modelType = modelRouter.classify(body?.model);
|
|
449
|
+
if (modelType === "unsupported_claude") {
|
|
450
|
+
throw localError(400, "model_not_supported", "The requested Claude model is not enabled for PM Studio");
|
|
451
|
+
}
|
|
452
|
+
if (modelType !== "claude") {
|
|
453
|
+
throw localError(
|
|
454
|
+
400,
|
|
455
|
+
"native_route_required",
|
|
456
|
+
"Non-Claude PM Studio requests must use the native GitHub Copilot origin",
|
|
457
|
+
);
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
const streaming = body?.stream === true;
|
|
443
461
|
const requestTimeoutMs = streaming ? streamHandshakeTimeoutMs : upstreamTimeoutMs;
|
|
444
462
|
const timeoutReason = streaming ? "stream_handshake_timeout" : "upstream_timeout";
|
|
445
463
|
abort.setTimeout(requestTimeoutMs, timeoutReason);
|
|
@@ -452,23 +470,12 @@ export function createPmStudioRelayHandler(options = {}) {
|
|
|
452
470
|
}
|
|
453
471
|
abort.setTimeout(requestTimeoutMs, timeoutReason);
|
|
454
472
|
|
|
455
|
-
const
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
}
|
|
459
|
-
|
|
460
|
-
let upstream;
|
|
461
|
-
if (modelType === "claude") {
|
|
462
|
-
upstream = await options.claudeClient.chatCompletions(body, {
|
|
463
|
-
signal: abort.signal,
|
|
464
|
-
fetchImpl: fetchClaudeUpstream,
|
|
465
|
-
});
|
|
466
|
-
} else {
|
|
467
|
-
const bodyText = JSON.stringify(body);
|
|
468
|
-
upstream = await fetchEnterprise(pathname, "POST", req.headers, token, bodyText, abort.signal);
|
|
469
|
-
}
|
|
473
|
+
const upstream = await options.claudeClient.chatCompletions(body, {
|
|
474
|
+
signal: abort.signal,
|
|
475
|
+
fetchImpl: fetchClaudeUpstream,
|
|
476
|
+
});
|
|
470
477
|
releaseRequest();
|
|
471
|
-
await relayUpstreamResponse(upstream,
|
|
478
|
+
await relayUpstreamResponse(upstream, body, res, abort, streamIdleTimeoutMs);
|
|
472
479
|
} finally {
|
|
473
480
|
releaseRequest();
|
|
474
481
|
}
|
|
@@ -494,7 +501,9 @@ export function createPmStudioRelayHandler(options = {}) {
|
|
|
494
501
|
}
|
|
495
502
|
|
|
496
503
|
const upstreamPath = pathname.slice(PM_STUDIO_RELAY_PREFIX.length);
|
|
497
|
-
const allowedMethod = upstreamPath === "/models"
|
|
504
|
+
const allowedMethod = upstreamPath === "/models"
|
|
505
|
+
? "GET"
|
|
506
|
+
: upstreamPath === CHAT_COMPLETIONS_PATH ? "POST" : null;
|
|
498
507
|
if (!allowedMethod) {
|
|
499
508
|
sendLocalError(res, 404, "route_not_found", "PM Studio relay route not found");
|
|
500
509
|
return true;
|
|
@@ -512,13 +521,14 @@ export function createPmStudioRelayHandler(options = {}) {
|
|
|
512
521
|
abort.setTimeout(upstreamTimeoutMs, "upstream_timeout");
|
|
513
522
|
await handleModels(req, res, token, abort);
|
|
514
523
|
} else {
|
|
515
|
-
await
|
|
524
|
+
await handleChatCompletions(req, res, token, abort);
|
|
516
525
|
}
|
|
517
526
|
} catch (error) {
|
|
518
527
|
const safe = safeFailure(error, abort);
|
|
519
528
|
if (res.headersSent) {
|
|
520
529
|
if (!res.destroyed) res.destroy();
|
|
521
530
|
} else {
|
|
531
|
+
res.setHeader(PM_STUDIO_RELAY_MARKER_HEADER, PM_STUDIO_RELAY_MARKER_VALUE);
|
|
522
532
|
sendJsonError(res, safe, safe.statusCode || 502);
|
|
523
533
|
}
|
|
524
534
|
} finally {
|
package/src/pm-studio-setup.mjs
CHANGED
|
@@ -530,8 +530,12 @@ export function inspectPmStudioApp({
|
|
|
530
530
|
&& asar.state === "patched"
|
|
531
531
|
&& integrityMatches(plistIntegrity, "SHA256", recipe.patchedHeaderSha256)
|
|
532
532
|
&& patchedSignatureAccepted;
|
|
533
|
+
const legacy = issues.length === 0
|
|
534
|
+
&& asar.state === "legacy"
|
|
535
|
+
&& integrityMatches(plistIntegrity, "SHA256", recipe.legacy?.headerSha256)
|
|
536
|
+
&& patchedSignatureAccepted;
|
|
533
537
|
|
|
534
|
-
if (!clean && !patched) {
|
|
538
|
+
if (!clean && !patched && !legacy) {
|
|
535
539
|
if (asar.state === "drift") issues.push("app.asar is unknown drift");
|
|
536
540
|
else if (asar.state === "clean"
|
|
537
541
|
&& !integrityMatches(plistIntegrity, "SHA256", recipe.sourceHeaderSha256)) {
|
|
@@ -539,6 +543,9 @@ export function inspectPmStudioApp({
|
|
|
539
543
|
} else if (asar.state === "patched"
|
|
540
544
|
&& !integrityMatches(plistIntegrity, "SHA256", recipe.patchedHeaderSha256)) {
|
|
541
545
|
issues.push("patched ASAR does not match ElectronAsarIntegrity");
|
|
546
|
+
} else if (asar.state === "legacy"
|
|
547
|
+
&& !integrityMatches(plistIntegrity, "SHA256", recipe.legacy?.headerSha256)) {
|
|
548
|
+
issues.push("legacy ASAR does not match ElectronAsarIntegrity");
|
|
542
549
|
}
|
|
543
550
|
if (asar.state === "clean" && recipe.sourceBundleContent && !exactSourceBundle) {
|
|
544
551
|
issues.push("bundle content does not match the exact clean recipe");
|
|
@@ -547,7 +554,9 @@ export function inspectPmStudioApp({
|
|
|
547
554
|
issues.push("codesign verification failed");
|
|
548
555
|
}
|
|
549
556
|
else if (asar.state === "clean" && codeSign.adHoc) issues.push("clean bundle has an unexpected ad-hoc signature");
|
|
550
|
-
else if (asar.state
|
|
557
|
+
else if (["patched", "legacy"].includes(asar.state) && !codeSign.adHoc) {
|
|
558
|
+
issues.push(`${asar.state} bundle is not ad-hoc signed`);
|
|
559
|
+
}
|
|
551
560
|
else if (asar.state === "clean" && codeSign.teamIdentifier !== recipe.sourceTeamIdentifier) {
|
|
552
561
|
issues.push(`clean bundle TeamIdentifier is ${codeSign.teamIdentifier || "missing"}, expected ${recipe.sourceTeamIdentifier}`);
|
|
553
562
|
}
|
|
@@ -559,15 +568,15 @@ export function inspectPmStudioApp({
|
|
|
559
568
|
&& executableIntegrity.frameworkSha256 !== recipe.sourceElectronFrameworkSha256) {
|
|
560
569
|
issues.push("Electron Framework hash does not match the clean recipe");
|
|
561
570
|
}
|
|
562
|
-
if (asar.state
|
|
571
|
+
if (["patched", "legacy"].includes(asar.state) && codeSign?.valid && codeSign?.adHoc
|
|
563
572
|
&& recipe.patchedSigningMetadata
|
|
564
573
|
&& !patchedSignatureAccepted) {
|
|
565
|
-
issues.push(
|
|
574
|
+
issues.push(`${asar.state} bundle signing metadata does not match the recipe`);
|
|
566
575
|
}
|
|
567
576
|
}
|
|
568
577
|
|
|
569
578
|
return {
|
|
570
|
-
state: clean ? "clean" : patched ? "patched" : "drift",
|
|
579
|
+
state: clean ? "clean" : patched ? "patched" : legacy ? "legacy" : "drift",
|
|
571
580
|
metadata: {
|
|
572
581
|
version: String(metadata.version),
|
|
573
582
|
build: String(metadata.build),
|
|
@@ -676,6 +685,23 @@ function backupManifest(recipe, createdAt) {
|
|
|
676
685
|
};
|
|
677
686
|
}
|
|
678
687
|
|
|
688
|
+
function patchRecordMatches(record, recipe, state) {
|
|
689
|
+
const expected = state === "legacy"
|
|
690
|
+
? recipe.legacy
|
|
691
|
+
: { asarSha256: recipe.patchedAsarSha256, headerSha256: recipe.patchedHeaderSha256 };
|
|
692
|
+
return Boolean(expected)
|
|
693
|
+
&& record?.asar_sha256 === expected.asarSha256
|
|
694
|
+
&& record?.asar_header_sha256 === expected.headerSha256
|
|
695
|
+
&& integrityMatches(record?.electron_asar_integrity, "SHA256", expected.headerSha256);
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
function patchRecordForState(manifest, recipe, state) {
|
|
699
|
+
if (state === "legacy" && patchRecordMatches(manifest?.legacy_patched, recipe, state)) {
|
|
700
|
+
return manifest.legacy_patched;
|
|
701
|
+
}
|
|
702
|
+
return patchRecordMatches(manifest?.patched, recipe, state) ? manifest.patched : null;
|
|
703
|
+
}
|
|
704
|
+
|
|
679
705
|
function validateBackup({ backupDir, recipe, operations }) {
|
|
680
706
|
const manifestPath = path.join(backupDir, "manifest.json");
|
|
681
707
|
const backupAppPath = path.join(backupDir, "PM Studio.app");
|
|
@@ -702,9 +728,8 @@ function validateBackup({ backupDir, recipe, operations }) {
|
|
|
702
728
|
&& JSON.stringify(manifest?.source?.bundle_content) !== JSON.stringify(recipe.sourceBundleContent))
|
|
703
729
|
|| (recipe.sourceArtifact
|
|
704
730
|
&& JSON.stringify(manifest?.source?.artifact) !== JSON.stringify(recipe.sourceArtifact))
|
|
705
|
-
|| manifest
|
|
706
|
-
|
|
707
|
-
|| !integrityMatches(manifest?.patched?.electron_asar_integrity, "SHA256", recipe.patchedHeaderSha256)) {
|
|
731
|
+
|| (!patchRecordForState(manifest, recipe, "patched")
|
|
732
|
+
&& !patchRecordForState(manifest, recipe, "legacy"))) {
|
|
708
733
|
throw setupError("PM_STUDIO_BACKUP_INVALID", `Existing PM Studio backup does not match recipe ${recipe.id}`);
|
|
709
734
|
}
|
|
710
735
|
const inspection = inspectPmStudioApp({ appPath: backupAppPath, recipe, operations });
|
|
@@ -750,12 +775,21 @@ function ensureBackup({ appPath, backupRoot, recipe, operations }) {
|
|
|
750
775
|
|
|
751
776
|
function writePatchedBinaryRecord({ backup, inspection, operations, recipe }) {
|
|
752
777
|
const manifest = JSON.parse(fs.readFileSync(backup.manifestPath, "utf8"));
|
|
753
|
-
manifest.
|
|
754
|
-
|
|
755
|
-
|
|
778
|
+
if (!patchRecordMatches(manifest.legacy_patched, recipe, "legacy")
|
|
779
|
+
&& patchRecordMatches(manifest.patched, recipe, "legacy")) {
|
|
780
|
+
manifest.legacy_patched = manifest.patched;
|
|
781
|
+
}
|
|
782
|
+
manifest.patched = {
|
|
783
|
+
asar_sha256: recipe.patchedAsarSha256,
|
|
784
|
+
asar_header_sha256: recipe.patchedHeaderSha256,
|
|
785
|
+
electron_asar_integrity: { algorithm: "SHA256", hash: recipe.patchedHeaderSha256 },
|
|
786
|
+
binaries: {
|
|
787
|
+
main_executable_sha256: inspection.executableIntegrity.executableSha256,
|
|
788
|
+
electron_framework_sha256: inspection.executableIntegrity.frameworkSha256,
|
|
789
|
+
},
|
|
790
|
+
signing_metadata: signingMetadata(inspection.codeSign),
|
|
791
|
+
...(recipe.sourceBundleContent ? { bundle_content: inspection.bundleContent } : {}),
|
|
756
792
|
};
|
|
757
|
-
manifest.patched.signing_metadata = signingMetadata(inspection.codeSign);
|
|
758
|
-
if (recipe.sourceBundleContent) manifest.patched.bundle_content = inspection.bundleContent;
|
|
759
793
|
const temporaryPath = `${backup.manifestPath}.${operations.uuid()}.tmp`;
|
|
760
794
|
try {
|
|
761
795
|
fs.writeFileSync(temporaryPath, `${JSON.stringify(manifest, null, 2)}\n`, { mode: 0o600 });
|
|
@@ -766,7 +800,12 @@ function writePatchedBinaryRecord({ backup, inspection, operations, recipe }) {
|
|
|
766
800
|
backup.manifest = manifest;
|
|
767
801
|
}
|
|
768
802
|
|
|
769
|
-
export function assertPatchedBinaryRecord({
|
|
803
|
+
export function assertPatchedBinaryRecord({
|
|
804
|
+
inspection,
|
|
805
|
+
manifestPath,
|
|
806
|
+
recipe,
|
|
807
|
+
recordState = inspection?.state === "legacy" ? "legacy" : "patched",
|
|
808
|
+
}) {
|
|
770
809
|
let manifest;
|
|
771
810
|
try {
|
|
772
811
|
manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8"));
|
|
@@ -790,23 +829,24 @@ export function assertPatchedBinaryRecord({ inspection, manifestPath, recipe })
|
|
|
790
829
|
|| JSON.stringify(manifest?.source?.bundle_content) !== JSON.stringify(recipe.sourceBundleContent)
|
|
791
830
|
))
|
|
792
831
|
|| (recipe.sourceArtifact
|
|
793
|
-
&& JSON.stringify(manifest?.source?.artifact) !== JSON.stringify(recipe.sourceArtifact))
|
|
794
|
-
|| manifest?.patched?.asar_sha256 !== recipe.patchedAsarSha256
|
|
795
|
-
|| manifest?.patched?.asar_header_sha256 !== recipe.patchedHeaderSha256
|
|
796
|
-
|| !integrityMatches(manifest?.patched?.electron_asar_integrity, "SHA256", recipe.patchedHeaderSha256))) {
|
|
832
|
+
&& JSON.stringify(manifest?.source?.artifact) !== JSON.stringify(recipe.sourceArtifact)))) {
|
|
797
833
|
throw setupError("PM_STUDIO_PATCH_RECORD_INVALID", "PM Studio patched-binary record does not match the installed patch recipe");
|
|
798
834
|
}
|
|
799
|
-
const
|
|
835
|
+
const record = patchRecordForState(manifest, recipe, recordState);
|
|
836
|
+
if (!record) {
|
|
837
|
+
throw setupError("PM_STUDIO_PATCH_RECORD_INVALID", `PM Studio ${recordState} record does not match the installed patch recipe`);
|
|
838
|
+
}
|
|
839
|
+
const binaries = record.binaries;
|
|
800
840
|
if (!binaries
|
|
801
841
|
|| binaries.main_executable_sha256 !== inspection.executableIntegrity.executableSha256
|
|
802
842
|
|| binaries.electron_framework_sha256 !== inspection.executableIntegrity.frameworkSha256) {
|
|
803
843
|
throw setupError("PM_STUDIO_BUNDLE_DRIFT", "PM Studio executable or Electron Framework differs from the installed patch record");
|
|
804
844
|
}
|
|
805
|
-
if (JSON.stringify(
|
|
845
|
+
if (JSON.stringify(record.signing_metadata) !== JSON.stringify(signingMetadata(inspection.codeSign))) {
|
|
806
846
|
throw setupError("PM_STUDIO_BUNDLE_DRIFT", "PM Studio signing metadata differs from the installed patch record");
|
|
807
847
|
}
|
|
808
848
|
if (recipe?.sourceBundleContent
|
|
809
|
-
&& JSON.stringify(
|
|
849
|
+
&& JSON.stringify(record.bundle_content) !== JSON.stringify(inspection.bundleContent)) {
|
|
810
850
|
throw setupError("PM_STUDIO_BUNDLE_DRIFT", "PM Studio bundle content differs from the installed patch record");
|
|
811
851
|
}
|
|
812
852
|
}
|
|
@@ -922,12 +962,13 @@ export async function runPmStudioSetup({
|
|
|
922
962
|
emit(`[OK] PM Studio ${recipe.version} build ${recipe.build} is already patched; no backup or signing was repeated.`);
|
|
923
963
|
emit(`[OK] Verified backup: ${backup.backupAppPath}`);
|
|
924
964
|
emit(`[OK] Backup manifest: ${backup.manifestPath}`);
|
|
925
|
-
emit(`[INFO] Start ${commandName} before
|
|
965
|
+
emit(`[INFO] Start ${commandName} before using Claude in PM Studio; GPT remains on PM Studio's native GitHub Copilot path.`);
|
|
926
966
|
return { status: "already_patched", changed: false, recipe: recipe.id, backup, inspection: source, messages };
|
|
927
967
|
}
|
|
928
|
-
|
|
968
|
+
const migratingLegacy = source.state === "legacy";
|
|
969
|
+
if (source.state !== "clean" && !migratingLegacy) {
|
|
929
970
|
throw setupError("PM_STUDIO_BUNDLE_DRIFT",
|
|
930
|
-
`PM Studio ${recipe.version} build ${recipe.build} does not match the clean or
|
|
971
|
+
`PM Studio ${recipe.version} build ${recipe.build} does not match the clean, split-origin, or legacy recipe; no files were changed`, source.issues);
|
|
931
972
|
}
|
|
932
973
|
|
|
933
974
|
if (source.sourceVerification === "exact-bundle-content") {
|
|
@@ -935,17 +976,36 @@ export async function runPmStudioSetup({
|
|
|
935
976
|
}
|
|
936
977
|
|
|
937
978
|
const backupExists = fs.existsSync(finalBackupDir);
|
|
979
|
+
let verifiedBackup = null;
|
|
980
|
+
if (migratingLegacy) {
|
|
981
|
+
if (!backupExists) {
|
|
982
|
+
throw setupError("PM_STUDIO_BACKUP_INVALID",
|
|
983
|
+
"Legacy PM Studio global-origin patch has no matching verified clean backup; no files were changed");
|
|
984
|
+
}
|
|
985
|
+
verifiedBackup = validateBackup({ backupDir: finalBackupDir, recipe, operations });
|
|
986
|
+
assertPatchedBinaryRecord({
|
|
987
|
+
inspection: source,
|
|
988
|
+
manifestPath: verifiedBackup.manifestPath,
|
|
989
|
+
recipe,
|
|
990
|
+
recordState: "legacy",
|
|
991
|
+
});
|
|
992
|
+
emit("[WARN] Legacy global-origin patch detected; setup will rebuild from the verified clean backup so GPT and enterprise authentication return to PM Studio's native origin.");
|
|
993
|
+
}
|
|
938
994
|
assertSpace({ appPath, backupRoot, backupExists, operations });
|
|
939
995
|
|
|
940
996
|
emit("[WARN] PM Studio's vendor signature will be replaced with an ad-hoc signature; an official update or reinstall may overwrite this patch.");
|
|
941
|
-
const backup = ensureBackup({ appPath, backupRoot, recipe, operations });
|
|
997
|
+
const backup = verifiedBackup || ensureBackup({ appPath, backupRoot, recipe, operations });
|
|
942
998
|
const stagePath = path.join(path.dirname(appPath), `.${path.basename(appPath)}.ccdx-stage-${operations.uuid()}`);
|
|
943
999
|
let preserveStage = false;
|
|
944
1000
|
try {
|
|
945
|
-
operations.copyBundle({
|
|
1001
|
+
operations.copyBundle({
|
|
1002
|
+
source: migratingLegacy ? backup.backupAppPath : appPath,
|
|
1003
|
+
destination: stagePath,
|
|
1004
|
+
processRunner: operations.processRunner,
|
|
1005
|
+
});
|
|
946
1006
|
const stagedSource = inspectPmStudioApp({ appPath: stagePath, recipe, operations });
|
|
947
1007
|
if (stagedSource.state !== "clean"
|
|
948
|
-
|| (recipe.sourceBundleContent
|
|
1008
|
+
|| (!migratingLegacy && recipe.sourceBundleContent
|
|
949
1009
|
&& stagedSource.bundleContent?.sha256 !== source.bundleContent?.sha256)) {
|
|
950
1010
|
throw setupError("PM_STUDIO_STAGE_INVALID", "PM Studio staging copy does not match the exact clean source bundle", stagedSource.issues);
|
|
951
1011
|
}
|
|
@@ -966,12 +1026,12 @@ export async function runPmStudioSetup({
|
|
|
966
1026
|
if (staged.state !== "patched") {
|
|
967
1027
|
throw setupError("PM_STUDIO_STAGE_VERIFY_FAILED", "Patched PM Studio staging bundle failed verification", staged.issues);
|
|
968
1028
|
}
|
|
969
|
-
assertSigningMetadataPreserved(
|
|
1029
|
+
assertSigningMetadataPreserved(stagedSource, staged, recipe);
|
|
970
1030
|
writePatchedBinaryRecord({ backup, inspection: staged, operations, recipe });
|
|
971
1031
|
|
|
972
1032
|
assertNoBlockingProcesses(operations, appPath);
|
|
973
1033
|
const unchangedSource = inspectPmStudioApp({ appPath, recipe, operations });
|
|
974
|
-
if (unchangedSource.state !==
|
|
1034
|
+
if (unchangedSource.state !== source.state
|
|
975
1035
|
|| unchangedSource.asar.asarSha256 !== source.asar.asarSha256
|
|
976
1036
|
|| unchangedSource.asar.headerSha256 !== source.asar.headerSha256
|
|
977
1037
|
|| (recipe.sourceBundleContent
|
|
@@ -989,12 +1049,17 @@ export async function runPmStudioSetup({
|
|
|
989
1049
|
`PM Studio replacement final state could not be verified. Do not launch it. ${recoveryMessage(backup)}${preserveStage ? ` Verified staging retained: ${stagePath}.` : ""}`);
|
|
990
1050
|
}
|
|
991
1051
|
if (installed.state !== "patched") {
|
|
992
|
-
|
|
993
|
-
|
|
1052
|
+
const originalRetained = installed.state === source.state
|
|
1053
|
+
&& installed.asar.asarSha256 === source.asar.asarSha256
|
|
1054
|
+
&& installed.asar.headerSha256 === source.asar.headerSha256
|
|
1055
|
+
&& (!recipe.sourceBundleContent
|
|
1056
|
+
|| installed.bundleContent?.sha256 === source.bundleContent?.sha256);
|
|
1057
|
+
preserveStage = !originalRetained && fs.existsSync(stagePath);
|
|
1058
|
+
const code = originalRetained
|
|
994
1059
|
? "PM_STUDIO_REPLACE_FAILED"
|
|
995
1060
|
: "PM_STUDIO_INSTALL_VERIFY_FAILED";
|
|
996
|
-
const stateMessage =
|
|
997
|
-
?
|
|
1061
|
+
const stateMessage = originalRetained
|
|
1062
|
+
? `The installed PM Studio remains the verified ${source.state} bundle`
|
|
998
1063
|
: "The installed PM Studio did not retain the verified patched state; do not launch it";
|
|
999
1064
|
throw setupError(code,
|
|
1000
1065
|
`${stateMessage}. ${recoveryMessage(backup)}${preserveStage ? ` Verified staging retained: ${stagePath}.` : ""}`,
|
|
@@ -1011,7 +1076,9 @@ export async function runPmStudioSetup({
|
|
|
1011
1076
|
emit("[WARN] Atomic replacement reported an error after the exact patched bundle became installed; the final bundle was independently verified.");
|
|
1012
1077
|
}
|
|
1013
1078
|
|
|
1014
|
-
emit(
|
|
1079
|
+
emit(migratingLegacy
|
|
1080
|
+
? `[OK] Migrated PM Studio ${recipe.version} build ${recipe.build} from the legacy global-origin patch to the split-origin patch.`
|
|
1081
|
+
: `[OK] Patched PM Studio ${recipe.version} build ${recipe.build}.`);
|
|
1015
1082
|
emit(`[OK] app.asar SHA-256: ${installed.asar.asarSha256}`);
|
|
1016
1083
|
emit(`[OK] Verified backup: ${backup.backupAppPath}${backup.reused ? " (reused)" : ""}`);
|
|
1017
1084
|
emit(`[OK] Backup manifest: ${backup.manifestPath}`);
|
|
@@ -1019,9 +1086,9 @@ export async function runPmStudioSetup({
|
|
|
1019
1086
|
emit(`[INFO] Restore step 1/3: quit PM Studio and its updater.`);
|
|
1020
1087
|
emit(`[INFO] Restore step 2/3: confirm the installed Info.plist still reports version/build ${recipe.version}/${recipe.build}; never restore this backup over another version.`);
|
|
1021
1088
|
emit(`[INFO] Restore step 3/3: move ${appPath} aside, copy ${backup.backupAppPath} to that exact path, and verify its signature before launch.`);
|
|
1022
|
-
emit(`[INFO] Start ${commandName} before
|
|
1089
|
+
emit(`[INFO] Start ${commandName} before using Claude in PM Studio; GPT remains on PM Studio's native GitHub Copilot path.`);
|
|
1023
1090
|
return {
|
|
1024
|
-
status: "patched",
|
|
1091
|
+
status: migratingLegacy ? "migrated" : "patched",
|
|
1025
1092
|
changed: true,
|
|
1026
1093
|
recipe: recipe.id,
|
|
1027
1094
|
backup,
|
package/src/pm-studio-status.mjs
CHANGED
|
@@ -16,6 +16,11 @@ import {
|
|
|
16
16
|
pmStudioPatchManifestPath,
|
|
17
17
|
} from "./pm-studio-setup.mjs";
|
|
18
18
|
import { readAdapterStatus } from "./cli-status.mjs";
|
|
19
|
+
import {
|
|
20
|
+
PM_STUDIO_RELAY_MARKER_HEADER,
|
|
21
|
+
PM_STUDIO_RELAY_MARKER_VALUE,
|
|
22
|
+
PM_STUDIO_RELAY_PREFIX,
|
|
23
|
+
} from "./pm-studio-relay.mjs";
|
|
19
24
|
import { checkRunningAdapter } from "./running-adapter.mjs";
|
|
20
25
|
import { status } from "./status.mjs";
|
|
21
26
|
import {
|
|
@@ -27,12 +32,11 @@ import {
|
|
|
27
32
|
|
|
28
33
|
const PM_RELAY_HOST = "127.0.0.1";
|
|
29
34
|
const PM_RELAY_PORT = 2026;
|
|
30
|
-
const
|
|
35
|
+
const PM_RELAY_PROBE_TIMEOUT_MS = 500;
|
|
36
|
+
const PM_RELAY_CAPABILITY = "pm_studio_split_origin_v1";
|
|
31
37
|
const PM_RELAY_ROUTE_NAMES = Object.freeze([
|
|
32
38
|
"pm_models",
|
|
33
39
|
"pm_chat_completions",
|
|
34
|
-
"pm_responses",
|
|
35
|
-
"pm_embeddings",
|
|
36
40
|
]);
|
|
37
41
|
|
|
38
42
|
function selectRecipe(metadata, recipes) {
|
|
@@ -45,6 +49,31 @@ function safeMessage(error) {
|
|
|
45
49
|
return String(error?.message || "inspection failed").replace(/\s+/g, " ").trim();
|
|
46
50
|
}
|
|
47
51
|
|
|
52
|
+
export async function probePmStudioRelay({
|
|
53
|
+
baseUrl,
|
|
54
|
+
fetchImpl = fetch,
|
|
55
|
+
timeoutMs = PM_RELAY_PROBE_TIMEOUT_MS,
|
|
56
|
+
} = {}) {
|
|
57
|
+
const controller = new AbortController();
|
|
58
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
59
|
+
try {
|
|
60
|
+
const response = await fetchImpl(new URL(`${PM_STUDIO_RELAY_PREFIX}/models`, baseUrl).href, {
|
|
61
|
+
method: "GET",
|
|
62
|
+
headers: { Accept: "application/json" },
|
|
63
|
+
redirect: "error",
|
|
64
|
+
signal: controller.signal,
|
|
65
|
+
});
|
|
66
|
+
const result = {
|
|
67
|
+
status: response.status,
|
|
68
|
+
marker: response.headers.get(PM_STUDIO_RELAY_MARKER_HEADER) || "",
|
|
69
|
+
};
|
|
70
|
+
await response.body?.cancel().catch(() => {});
|
|
71
|
+
return result;
|
|
72
|
+
} finally {
|
|
73
|
+
clearTimeout(timer);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
48
77
|
function inspectClaudeProfile(home, readCredentials) {
|
|
49
78
|
try {
|
|
50
79
|
const credentials = readCredentials(AUTH_PROFILE_CLAUDE, { home });
|
|
@@ -73,7 +102,7 @@ function inspectAdapterRuntime(adapter, runtimeStatus) {
|
|
|
73
102
|
issues.push("health and runtime status do not describe the same adapter instance");
|
|
74
103
|
}
|
|
75
104
|
const capabilities = new Set(Array.isArray(data.capabilities) ? data.capabilities : []);
|
|
76
|
-
if (!capabilities.has(PM_RELAY_CAPABILITY)) issues.push("PM Studio relay capability is missing");
|
|
105
|
+
if (!capabilities.has(PM_RELAY_CAPABILITY)) issues.push("PM Studio split-origin relay capability is missing");
|
|
77
106
|
if (data.profiles?.claude?.mode !== "isolated") issues.push("Claude profile mode is not isolated");
|
|
78
107
|
if (data.profiles?.claude?.profile_current !== true) {
|
|
79
108
|
issues.push("running Claude credentials do not match the current isolated profile");
|
|
@@ -93,6 +122,21 @@ function inspectAdapterRuntime(adapter, runtimeStatus) {
|
|
|
93
122
|
};
|
|
94
123
|
}
|
|
95
124
|
|
|
125
|
+
function inspectRelayProbe(runtime, probe) {
|
|
126
|
+
const relayProbe = {
|
|
127
|
+
status: probe?.status,
|
|
128
|
+
marker: String(probe?.marker || ""),
|
|
129
|
+
};
|
|
130
|
+
const issues = [...runtime.issues];
|
|
131
|
+
if (relayProbe.status !== 401) {
|
|
132
|
+
issues.push(`PM Studio relay probe returned HTTP ${relayProbe.status ?? "unknown"}; expected 401`);
|
|
133
|
+
}
|
|
134
|
+
if (relayProbe.marker !== PM_STUDIO_RELAY_MARKER_VALUE) {
|
|
135
|
+
issues.push("PM Studio relay compatibility marker is missing or incompatible");
|
|
136
|
+
}
|
|
137
|
+
return { ...runtime, relayProbe, ok: issues.length === 0, issues };
|
|
138
|
+
}
|
|
139
|
+
|
|
96
140
|
export async function inspectPmStudioStatus({
|
|
97
141
|
appPath = DEFAULT_PM_STUDIO_APP_PATH,
|
|
98
142
|
home = os.homedir(),
|
|
@@ -105,6 +149,7 @@ export async function inspectPmStudioStatus({
|
|
|
105
149
|
readClaudeCredentials = readAuthProfileCredentials,
|
|
106
150
|
checkRunningAdapterFn = checkRunningAdapter,
|
|
107
151
|
readAdapterStatusFn = readAdapterStatus,
|
|
152
|
+
probePmStudioRelayFn = probePmStudioRelay,
|
|
108
153
|
} = {}) {
|
|
109
154
|
const operations = createPmStudioSetupOperations(operationOverrides);
|
|
110
155
|
let app;
|
|
@@ -163,6 +208,18 @@ export async function inspectPmStudioStatus({
|
|
|
163
208
|
} catch (error) {
|
|
164
209
|
runtime = { ok: false, baseUrl: adapter.baseUrl, data: null, issues: [safeMessage(error)] };
|
|
165
210
|
}
|
|
211
|
+
if (runtime.data) {
|
|
212
|
+
try {
|
|
213
|
+
runtime = inspectRelayProbe(runtime, await probePmStudioRelayFn({ baseUrl: adapter.baseUrl }));
|
|
214
|
+
} catch (error) {
|
|
215
|
+
runtime = {
|
|
216
|
+
...runtime,
|
|
217
|
+
ok: false,
|
|
218
|
+
relayProbe: null,
|
|
219
|
+
issues: [...runtime.issues, `PM Studio relay probe failed: ${safeMessage(error)}`],
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
}
|
|
166
223
|
}
|
|
167
224
|
const operational = app.state === "patched"
|
|
168
225
|
&& app.patchRecord?.valid === true
|
|
@@ -182,6 +239,9 @@ function appStatusItem(result, commandName) {
|
|
|
182
239
|
const reason = app.patchRecord?.reason || "installed patch record is missing";
|
|
183
240
|
return { kind: "err", component: "App patch", detail: `${version}; patch record not verified`, message: `PM Studio ${version} matches the patch recipe, but its installed patch record is not verified: ${reason}` };
|
|
184
241
|
}
|
|
242
|
+
if (app.state === "legacy") {
|
|
243
|
+
return { kind: "err", component: "App patch", detail: `${version}; legacy global-origin patch`, message: `PM Studio ${version} has a legacy global-origin patch; run ${commandName} pms setup to migrate` };
|
|
244
|
+
}
|
|
185
245
|
if (app.state === "clean") return { kind: "warn", component: "App patch", detail: `${version}; supported, not patched`, message: `PM Studio ${version} is supported but not patched; run ${commandName} pms setup` };
|
|
186
246
|
if (app.state === "unsupported") return { kind: "err", component: "App patch", detail: `${version}; unsupported version`, message: `PM Studio ${version} has no exact patch recipe; no files will be changed` };
|
|
187
247
|
if (app.state === "drift" && app.patchRecipeMatched) {
|
|
@@ -203,7 +263,7 @@ function pmStudioStatusItems(result, commandName) {
|
|
|
203
263
|
items.push({ kind: "err", component: "Claude profile", detail: "not ready", message: `Isolated Claude profile is not ready${reason}; run ${PM_STUDIO_CLAUDE_AUTH_COMMAND}` });
|
|
204
264
|
}
|
|
205
265
|
if (result.adapter?.ok && result.runtime?.ok) {
|
|
206
|
-
items.push({ kind: "ok", component: "PM relay", detail: `verified at ${result.adapter.baseUrl}`, message: `PM
|
|
266
|
+
items.push({ kind: "ok", component: "PM relay", detail: `verified at ${result.adapter.baseUrl}`, message: `PM model discovery and isolated Claude routing are verified at ${result.adapter.baseUrl}` });
|
|
207
267
|
} else if (result.adapter?.ok) {
|
|
208
268
|
const reason = result.runtime?.issues?.join("; ") || "runtime status is unavailable";
|
|
209
269
|
items.push({ kind: "err", component: "PM relay", detail: "running but routing not ready", message: `The adapter is running at ${result.adapter.baseUrl}, but PM relay routing is not ready: ${reason}; stop it and run ${commandName} start` });
|
|
@@ -213,9 +273,9 @@ function pmStudioStatusItems(result, commandName) {
|
|
|
213
273
|
items.push({ kind: "warn", component: "PM relay", detail: `not running at ${PM_RELAY_HOST}:${PM_RELAY_PORT}`, message: `PM relay is not running at http://${PM_RELAY_HOST}:${PM_RELAY_PORT}; run ${commandName} start` });
|
|
214
274
|
}
|
|
215
275
|
if (result.runtime?.ok) {
|
|
216
|
-
items.push({ kind: "info", component: "Routing", detail: "GPT ->
|
|
276
|
+
items.push({ kind: "info", component: "Routing", detail: "GPT -> native official; models/Claude -> local", message: "Routing: PM GPT uses its native official GitHub Copilot path; model discovery and eligible Claude chat use the local CCDX relay" });
|
|
217
277
|
} else {
|
|
218
|
-
items.push({ kind: "info", component: "Routing", detail: "expected: GPT ->
|
|
278
|
+
items.push({ kind: "info", component: "Routing", detail: "expected: GPT -> native official; models/Claude -> local", message: "Expected routing: PM GPT uses its native official GitHub Copilot path; model discovery and eligible Claude chat use the local CCDX relay" });
|
|
219
279
|
}
|
|
220
280
|
return items;
|
|
221
281
|
}
|
|
@@ -65,9 +65,21 @@ function replayableResponsesOutputItems(output) {
|
|
|
65
65
|
return output.filter((item) => item && typeof item === "object");
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
+
function isEncryptedContentPart(value) {
|
|
69
|
+
return value && typeof value === "object" && value.type === "encrypted_content";
|
|
70
|
+
}
|
|
71
|
+
|
|
68
72
|
function stripEncryptedReasoningValue(value, state) {
|
|
69
73
|
if (Array.isArray(value)) {
|
|
70
|
-
|
|
74
|
+
const out = [];
|
|
75
|
+
for (const item of value) {
|
|
76
|
+
if (isEncryptedContentPart(item)) {
|
|
77
|
+
state.changed = true;
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
out.push(stripEncryptedReasoningValue(item, state));
|
|
81
|
+
}
|
|
82
|
+
return out;
|
|
71
83
|
}
|
|
72
84
|
|
|
73
85
|
if (value && typeof value === "object") {
|
|
@@ -87,7 +99,9 @@ function stripEncryptedReasoningValue(value, state) {
|
|
|
87
99
|
|
|
88
100
|
function isEncryptedReasoningInputItem(item) {
|
|
89
101
|
return item && typeof item === "object"
|
|
90
|
-
&& (item.type === "reasoning"
|
|
102
|
+
&& (item.type === "reasoning"
|
|
103
|
+
|| item.type === "encrypted_content"
|
|
104
|
+
|| Object.prototype.hasOwnProperty.call(item, "encrypted_content"));
|
|
91
105
|
}
|
|
92
106
|
|
|
93
107
|
export function sanitizeEncryptedReasoningRequest(reqContext) {
|
package/src/running-adapter.mjs
CHANGED
|
@@ -5,7 +5,9 @@ export const ADAPTER_HEALTH_PATH = "/_ccdx/health";
|
|
|
5
5
|
export const ADAPTER_STATUS_PATH = "/_ccdx/status";
|
|
6
6
|
export const ADAPTER_PROTOCOL_VERSION = 2;
|
|
7
7
|
export const ADAPTER_VERSION = localPackageVersion();
|
|
8
|
-
export const ADAPTER_CAPABILITIES = Object.freeze([
|
|
8
|
+
export const ADAPTER_CAPABILITIES = Object.freeze([
|
|
9
|
+
"pm_studio_split_origin_v1",
|
|
10
|
+
]);
|
|
9
11
|
const ADAPTER_INSTANCE_ID = randomUUID();
|
|
10
12
|
|
|
11
13
|
export function adapterHealthPayload() {
|