codex-copilot-dx 0.6.8 → 0.6.10
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/doctor.mjs +6 -0
- package/src/observability.mjs +0 -4
- package/src/pm-studio-asar.mjs +187 -84
- package/src/pm-studio-relay.mjs +40 -30
- package/src/pm-studio-setup.mjs +144 -36
- package/src/pm-studio-status.mjs +77 -9
- 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/doctor.mjs
CHANGED
|
@@ -590,6 +590,12 @@ export async function collectDoctorChecks({
|
|
|
590
590
|
fix: "stop the running adapter, then run ccdx start",
|
|
591
591
|
});
|
|
592
592
|
}
|
|
593
|
+
} else if (pm.app.state === "predecessor") {
|
|
594
|
+
checks.push({
|
|
595
|
+
kind: "warn",
|
|
596
|
+
message: `PM Studio ${version} has a verified predecessor split patch`,
|
|
597
|
+
fix: "ccdx pms setup",
|
|
598
|
+
});
|
|
593
599
|
} else if (pm.app.state === "clean") {
|
|
594
600
|
checks.push({
|
|
595
601
|
kind: "warn",
|
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);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,62 @@ 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
|
-
|
|
27
|
-
|
|
38
|
+
patchedAsarSha256: "799dc18103a4553daefcaaddbe3410a1bfaf68d84407f4c10ef3d996afd71ad0",
|
|
39
|
+
patchedHeaderSha256: "08253795176d5471c6f406a29ccb134875d503bb55bca909b2cd131cd7708510",
|
|
40
|
+
predecessor: Object.freeze({
|
|
41
|
+
id: "split-origin-v1-model-discovery-750ms",
|
|
42
|
+
kind: "split-origin-predecessor",
|
|
43
|
+
asarSha256: "d520d115604225c1a3feb749dbe29ad0d2cd175c5233c010de0e1fade527fa0b",
|
|
44
|
+
headerSha256: "83e5fba3b6751b3098d8f6901db361a9b620ab3b924e0e551d3df612ff412642",
|
|
45
|
+
signingMetadata: Object.freeze({
|
|
46
|
+
identifier: "com.pm-studio.app",
|
|
47
|
+
flags: "0x10002(adhoc,runtime)",
|
|
48
|
+
runtime_version: "26.0.0",
|
|
49
|
+
entitlements_sha256: "9d4ccbda4fe0c81a70df3db93b3e61fe0500f67f14cdcbee4dea230e6512d05c",
|
|
50
|
+
}),
|
|
51
|
+
}),
|
|
28
52
|
targets: Object.freeze([
|
|
29
53
|
Object.freeze({
|
|
30
54
|
path: "dist/main/main.js",
|
|
31
55
|
offset: 101_621_627,
|
|
32
56
|
size: 9_909_974,
|
|
33
|
-
sentinelOffset: 1_120_480,
|
|
34
|
-
absoluteSentinelOffset: 106_574_871,
|
|
35
|
-
sentinelCount: 1,
|
|
36
57
|
blockSize: 4_194_304,
|
|
37
58
|
sourceSha256: "58383be112886becdb8d8e7cce1b3efce07be6716190aa4940627f456eb8edb2",
|
|
38
|
-
patchedSha256: "
|
|
59
|
+
patchedSha256: "b219d08290b208fe41fa29b1e8ac4a1dee789e15f68fd4ff01eb5cd21249099b",
|
|
60
|
+
predecessorSha256: "69156040fb233a835b7be2f86fbe2f653380f03379adfedf46c63ab45de9d39d",
|
|
39
61
|
sourceBlocks: Object.freeze([
|
|
40
62
|
"3ce05a21e8992ccf12966cf9b78fcd28b2f66e6786489b5bb0a06f1d4b2a0c1e",
|
|
41
63
|
"0a4832413d8d8cdda67c8bb318c0e5c25df20c75802d637097ffd045a31cd443",
|
|
42
64
|
"bcc10582d8a74717f8f81b98182d3551f1eaf23eb9cadd908d160ce5e4ef8cba",
|
|
43
65
|
]),
|
|
44
66
|
patchedBlocks: Object.freeze([
|
|
45
|
-
"
|
|
67
|
+
"dcbffb0f3aaa79a5ca8f1ee9170826b874d9db237c230abcdc0490335b2dc6a8",
|
|
46
68
|
"0a4832413d8d8cdda67c8bb318c0e5c25df20c75802d637097ffd045a31cd443",
|
|
47
69
|
"bcc10582d8a74717f8f81b98182d3551f1eaf23eb9cadd908d160ce5e4ef8cba",
|
|
48
70
|
]),
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
size: 2_469_533,
|
|
54
|
-
sentinelOffset: 489_935,
|
|
55
|
-
absoluteSentinelOffset: 141_324_929,
|
|
56
|
-
sentinelCount: 1,
|
|
57
|
-
blockSize: 4_194_304,
|
|
58
|
-
sourceSha256: "eb63abf35be3db101da179789e8ffdcc5b67131258ac8d7976109713ff0cc994",
|
|
59
|
-
patchedSha256: "89bbea9516a914da3fa4f32befd33d0dbb0e63b09545e4d85392bccb52adc145",
|
|
60
|
-
sourceBlocks: Object.freeze([
|
|
61
|
-
"eb63abf35be3db101da179789e8ffdcc5b67131258ac8d7976109713ff0cc994",
|
|
62
|
-
]),
|
|
63
|
-
patchedBlocks: Object.freeze([
|
|
64
|
-
"89bbea9516a914da3fa4f32befd33d0dbb0e63b09545e4d85392bccb52adc145",
|
|
71
|
+
predecessorBlocks: Object.freeze([
|
|
72
|
+
"f0b5585d2ff0d403e5c4d35e9bd0b8cbfec5c41a9b9057b8cc808107e889cf46",
|
|
73
|
+
"0a4832413d8d8cdda67c8bb318c0e5c25df20c75802d637097ffd045a31cd443",
|
|
74
|
+
"bcc10582d8a74717f8f81b98182d3551f1eaf23eb9cadd908d160ce5e4ef8cba",
|
|
65
75
|
]),
|
|
76
|
+
edit: Object.freeze({
|
|
77
|
+
offset: 1_120_359,
|
|
78
|
+
absoluteOffset: 106_574_750,
|
|
79
|
+
length: PM_STUDIO_COPILOT_CONFIG_MODULE_LENGTH,
|
|
80
|
+
anchor: "47024(I,e,t){",
|
|
81
|
+
anchorCount: 1,
|
|
82
|
+
sourceSha256: PM_STUDIO_COPILOT_CONFIG_MODULE_SOURCE_SHA256,
|
|
83
|
+
patchedSha256: "873d2dd276bd12a7dc2876f5eaf8c67d4ae34749ead8911d0588badf4e9fc0cf",
|
|
84
|
+
predecessorSha256: "a5b9312ed78e8a7fc3bd38f7ea5d4ab008f21bae18ba3dd6aa2d4be6b69191db",
|
|
85
|
+
replacement: PM_STUDIO_SPLIT_ORIGIN_CONFIG_MODULE,
|
|
86
|
+
}),
|
|
66
87
|
}),
|
|
67
88
|
]),
|
|
89
|
+
legacy: Object.freeze({
|
|
90
|
+
kind: "global-origin-replacement",
|
|
91
|
+
asarSha256: "3dd0f53cdaa35a644d2cf56e4fc2dd20f5c90dc2989b6d81a467ef00ebb620a7",
|
|
92
|
+
headerSha256: "86ed1113a563b82ecec05de89aab87a5fd1666f35bee76e2d76e2ed5f6dcb3fa",
|
|
93
|
+
}),
|
|
68
94
|
});
|
|
69
95
|
|
|
70
96
|
export const PM_STUDIO_2_9_10_RECIPE = Object.freeze({
|
|
@@ -117,50 +143,62 @@ export const PM_STUDIO_2_9_10_RECIPE = Object.freeze({
|
|
|
117
143
|
dataOffset: 3_833_292,
|
|
118
144
|
sourceAsarSha256: "d243860770e8b1d8044213924f9704d1fc52f900d6c33461eff6358962330b78",
|
|
119
145
|
sourceHeaderSha256: "5fc77d5b61e9647fba10fd2bd5752d0889c14da5a00b21d31bc48fdcccf74a69",
|
|
120
|
-
patchedAsarSha256: "
|
|
121
|
-
patchedHeaderSha256: "
|
|
122
|
-
|
|
123
|
-
|
|
146
|
+
patchedAsarSha256: "f309fc7e86bb4edaf1fecd9061fb4ddc634357c0ba9299f1d13f8b2ff33efd90",
|
|
147
|
+
patchedHeaderSha256: "98c4af8656e61cc1a1a8b2188592a499a987136f8e6b763b0e83abda788e798d",
|
|
148
|
+
predecessor: Object.freeze({
|
|
149
|
+
id: "split-origin-v1-model-discovery-750ms",
|
|
150
|
+
kind: "split-origin-predecessor",
|
|
151
|
+
asarSha256: "ea28d998056b32ca4115d208a4d81ce629c83f3f5f89c6a66d50749849beb6fc",
|
|
152
|
+
headerSha256: "348d80f5f2d1ebb92e6089195b1703b87467678f80d8946b8ab83b3ead03e5be",
|
|
153
|
+
signingMetadata: Object.freeze({
|
|
154
|
+
identifier: "com.pm-studio.app",
|
|
155
|
+
flags: "0x10002(adhoc,runtime)",
|
|
156
|
+
runtime_version: "26.0.0",
|
|
157
|
+
entitlements_sha256: "9d4ccbda4fe0c81a70df3db93b3e61fe0500f67f14cdcbee4dea230e6512d05c",
|
|
158
|
+
}),
|
|
159
|
+
}),
|
|
124
160
|
targets: Object.freeze([
|
|
125
161
|
Object.freeze({
|
|
126
162
|
path: "dist/main/main.js",
|
|
127
163
|
offset: 101_622_115,
|
|
128
164
|
size: 10_150_157,
|
|
129
|
-
sentinelOffset: 1_241_344,
|
|
130
|
-
absoluteSentinelOffset: 106_696_751,
|
|
131
|
-
sentinelCount: 1,
|
|
132
165
|
blockSize: 4_194_304,
|
|
133
166
|
sourceSha256: "03607bb1edc068396b99f02b9c9e0d8e10388fb658afc112e06e875c123dbc27",
|
|
134
|
-
patchedSha256: "
|
|
167
|
+
patchedSha256: "71e936c79c3560ab5bd9813479f8a66f4a7b06629edca74953feaaf7904a6564",
|
|
168
|
+
predecessorSha256: "b5b5e37af3d06ffc603bdef620fc1e0254580f0ee325f1a77096af6c78379b79",
|
|
135
169
|
sourceBlocks: Object.freeze([
|
|
136
170
|
"3910a9bc5459f493956aafee7b3644e8d242027758f0dcdea97d7c10a324c6bf",
|
|
137
171
|
"49fa64c68425d35b1baff0d5745d856d377e014a7a7829322cd1b39aade9532e",
|
|
138
172
|
"bd7cc14a1d2e02abe66c0ec6fc4e97b263cfc1f67c4ae39e956c06d3bcc2fe67",
|
|
139
173
|
]),
|
|
140
174
|
patchedBlocks: Object.freeze([
|
|
141
|
-
"
|
|
175
|
+
"203ad1efbdcd869314510086632fdb0089196eb25179c46fb4108133cc65412f",
|
|
142
176
|
"49fa64c68425d35b1baff0d5745d856d377e014a7a7829322cd1b39aade9532e",
|
|
143
177
|
"bd7cc14a1d2e02abe66c0ec6fc4e97b263cfc1f67c4ae39e956c06d3bcc2fe67",
|
|
144
178
|
]),
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
size: 2_520_547,
|
|
150
|
-
sentinelOffset: 528_919,
|
|
151
|
-
absoluteSentinelOffset: 141_629_203,
|
|
152
|
-
sentinelCount: 1,
|
|
153
|
-
blockSize: 4_194_304,
|
|
154
|
-
sourceSha256: "3d123f7a3eb91777f723fe2c4d412677e6722b6348373e4b4bfda41dda8b60ad",
|
|
155
|
-
patchedSha256: "dc4a9d550f4beb3a2cd7ad0fa6fc82286106762bdf789189d8f88abd0a90f619",
|
|
156
|
-
sourceBlocks: Object.freeze([
|
|
157
|
-
"3d123f7a3eb91777f723fe2c4d412677e6722b6348373e4b4bfda41dda8b60ad",
|
|
158
|
-
]),
|
|
159
|
-
patchedBlocks: Object.freeze([
|
|
160
|
-
"dc4a9d550f4beb3a2cd7ad0fa6fc82286106762bdf789189d8f88abd0a90f619",
|
|
179
|
+
predecessorBlocks: Object.freeze([
|
|
180
|
+
"63aa15d8c44614a7b2d9656ddba23109d40bab66b66b36339154953bd164da75",
|
|
181
|
+
"49fa64c68425d35b1baff0d5745d856d377e014a7a7829322cd1b39aade9532e",
|
|
182
|
+
"bd7cc14a1d2e02abe66c0ec6fc4e97b263cfc1f67c4ae39e956c06d3bcc2fe67",
|
|
161
183
|
]),
|
|
184
|
+
edit: Object.freeze({
|
|
185
|
+
offset: 1_241_223,
|
|
186
|
+
absoluteOffset: 106_696_630,
|
|
187
|
+
length: PM_STUDIO_COPILOT_CONFIG_MODULE_LENGTH,
|
|
188
|
+
anchor: "47024(I,e,t){",
|
|
189
|
+
anchorCount: 1,
|
|
190
|
+
sourceSha256: PM_STUDIO_COPILOT_CONFIG_MODULE_SOURCE_SHA256,
|
|
191
|
+
patchedSha256: "873d2dd276bd12a7dc2876f5eaf8c67d4ae34749ead8911d0588badf4e9fc0cf",
|
|
192
|
+
predecessorSha256: "a5b9312ed78e8a7fc3bd38f7ea5d4ab008f21bae18ba3dd6aa2d4be6b69191db",
|
|
193
|
+
replacement: PM_STUDIO_SPLIT_ORIGIN_CONFIG_MODULE,
|
|
194
|
+
}),
|
|
162
195
|
}),
|
|
163
196
|
]),
|
|
197
|
+
legacy: Object.freeze({
|
|
198
|
+
kind: "global-origin-replacement",
|
|
199
|
+
asarSha256: "49f72d999a6085102341c2d551577e164db6f22e4707d2112efa3fd280e7315e",
|
|
200
|
+
headerSha256: "b264ece5b8e469cd3885a7a91f904f1902e1991f214ecbf228dfe91059ac7008",
|
|
201
|
+
}),
|
|
164
202
|
});
|
|
165
203
|
|
|
166
204
|
export const PM_STUDIO_RECIPES = Object.freeze([
|
|
@@ -334,7 +372,7 @@ function addMismatch(issues, label, actual, expected) {
|
|
|
334
372
|
if (actual !== expected) issues.push(`${label}: expected ${expected}, got ${actual}`);
|
|
335
373
|
}
|
|
336
374
|
|
|
337
|
-
function inspectTarget(parsed, target
|
|
375
|
+
function inspectTarget(parsed, target) {
|
|
338
376
|
const entry = findAsarEntry(parsed.header, target.path);
|
|
339
377
|
if (!entry) return { path: target.path, missing: true };
|
|
340
378
|
|
|
@@ -352,8 +390,28 @@ function inspectTarget(parsed, target, recipe) {
|
|
|
352
390
|
}
|
|
353
391
|
|
|
354
392
|
const bytes = parsed.buffer.subarray(absoluteOffset, absoluteOffset + size);
|
|
355
|
-
|
|
356
|
-
|
|
393
|
+
let edit = null;
|
|
394
|
+
if (target.edit) {
|
|
395
|
+
let editOffset;
|
|
396
|
+
let editLength;
|
|
397
|
+
try {
|
|
398
|
+
editOffset = integer(target.edit.offset, `${target.path} edit offset`);
|
|
399
|
+
editLength = integer(target.edit.length, `${target.path} edit length`);
|
|
400
|
+
} catch (error) {
|
|
401
|
+
return { path: target.path, offset, size, absoluteOffset, malformed: error.message };
|
|
402
|
+
}
|
|
403
|
+
if (editLength === 0 || editOffset + editLength > bytes.length) {
|
|
404
|
+
return { path: target.path, offset, size, absoluteOffset, malformed: "edit window is outside the target" };
|
|
405
|
+
}
|
|
406
|
+
const anchor = Buffer.from(target.edit.anchor || "");
|
|
407
|
+
edit = {
|
|
408
|
+
offset: editOffset,
|
|
409
|
+
absoluteOffset: absoluteOffset + editOffset,
|
|
410
|
+
length: editLength,
|
|
411
|
+
sha256: sha256Hex(bytes.subarray(editOffset, editOffset + editLength)),
|
|
412
|
+
anchorPositions: occurrencePositions(bytes, anchor),
|
|
413
|
+
};
|
|
414
|
+
}
|
|
357
415
|
return {
|
|
358
416
|
path: target.path,
|
|
359
417
|
offset,
|
|
@@ -363,41 +421,48 @@ function inspectTarget(parsed, target, recipe) {
|
|
|
363
421
|
blockSize: entry.integrity?.blockSize,
|
|
364
422
|
blocks: blockSha256(bytes, target.blockSize),
|
|
365
423
|
integrity: entry.integrity || null,
|
|
366
|
-
|
|
367
|
-
patchedSentinelPositions: occurrencePositions(bytes, patchedSentinel),
|
|
424
|
+
edit,
|
|
368
425
|
};
|
|
369
426
|
}
|
|
370
427
|
|
|
371
|
-
function targetStateIssues(detail, target,
|
|
428
|
+
function targetStateIssues(detail, target, state) {
|
|
372
429
|
const issues = [];
|
|
373
430
|
if (detail.missing) return [`${target.path}: entry is missing`];
|
|
374
431
|
if (detail.malformed) return [`${target.path}: ${detail.malformed}`];
|
|
375
432
|
|
|
376
433
|
addMismatch(issues, `${target.path} offset`, detail.offset, target.offset);
|
|
377
434
|
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
435
|
addMismatch(issues, `${target.path} integrity algorithm`,
|
|
381
436
|
detail.integrity?.algorithm, "SHA256");
|
|
382
437
|
addMismatch(issues, `${target.path} block size`, detail.integrity?.blockSize, target.blockSize);
|
|
383
438
|
|
|
384
439
|
const patched = state === "patched";
|
|
385
|
-
const
|
|
386
|
-
const
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
const
|
|
440
|
+
const predecessor = state === "predecessor";
|
|
441
|
+
const expectedHash = patched
|
|
442
|
+
? target.patchedSha256
|
|
443
|
+
: predecessor ? target.predecessorSha256 : target.sourceSha256;
|
|
444
|
+
const expectedBlocks = patched
|
|
445
|
+
? target.patchedBlocks
|
|
446
|
+
: predecessor ? target.predecessorBlocks : target.sourceBlocks;
|
|
390
447
|
|
|
391
448
|
addMismatch(issues, `${target.path} SHA-256`, detail.sha256, expectedHash);
|
|
392
449
|
addMismatch(issues, `${target.path} integrity hash`, detail.integrity?.hash, expectedHash);
|
|
393
450
|
if (!sameArray(detail.blocks, expectedBlocks)) issues.push(`${target.path} computed block hashes do not match ${state} recipe`);
|
|
394
451
|
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
|
-
|
|
452
|
+
if (target.edit) {
|
|
453
|
+
addMismatch(issues, `${target.path} edit offset`, detail.edit?.offset, target.edit.offset);
|
|
454
|
+
addMismatch(issues, `${target.path} absolute edit offset`, detail.edit?.absoluteOffset, target.edit.absoluteOffset);
|
|
455
|
+
addMismatch(issues, `${target.path} edit length`, detail.edit?.length, target.edit.length);
|
|
456
|
+
addMismatch(issues, `${target.path} edit SHA-256`, detail.edit?.sha256,
|
|
457
|
+
patched
|
|
458
|
+
? target.edit.patchedSha256
|
|
459
|
+
: predecessor ? target.edit.predecessorSha256 : target.edit.sourceSha256);
|
|
460
|
+
if (!sameArray(detail.edit?.anchorPositions, [target.edit.offset])) {
|
|
461
|
+
issues.push(`${target.path} module anchor position/count does not match recipe`);
|
|
462
|
+
}
|
|
463
|
+
if (detail.edit?.anchorPositions?.length !== target.edit.anchorCount) {
|
|
464
|
+
issues.push(`${target.path} module anchor count is ${detail.edit?.anchorPositions?.length || 0}, expected ${target.edit.anchorCount}`);
|
|
465
|
+
}
|
|
401
466
|
}
|
|
402
467
|
return issues;
|
|
403
468
|
}
|
|
@@ -406,26 +471,44 @@ export function inspectAsarBuffer(value, recipe = PM_STUDIO_2_9_7_RECIPE) {
|
|
|
406
471
|
const parsed = parseAsarBuffer(value);
|
|
407
472
|
const asarSha256 = sha256Hex(parsed.buffer);
|
|
408
473
|
const headerSha256 = sha256Hex(parsed.headerBytes);
|
|
409
|
-
const targets = recipe.targets.map((target) => inspectTarget(parsed, target
|
|
474
|
+
const targets = recipe.targets.map((target) => inspectTarget(parsed, target));
|
|
410
475
|
const commonIssues = [];
|
|
411
476
|
addMismatch(commonIssues, "ASAR data offset", parsed.dataOffset, recipe.dataOffset);
|
|
412
477
|
|
|
413
478
|
const issuesFor = (state) => {
|
|
414
479
|
const patched = state === "patched";
|
|
480
|
+
const predecessor = state === "predecessor";
|
|
481
|
+
const expected = predecessor ? recipe.predecessor : null;
|
|
415
482
|
const issues = [...commonIssues];
|
|
416
483
|
addMismatch(issues, "ASAR SHA-256", asarSha256,
|
|
417
|
-
patched ? recipe.patchedAsarSha256 : recipe.sourceAsarSha256);
|
|
484
|
+
patched ? recipe.patchedAsarSha256 : predecessor ? expected?.asarSha256 : recipe.sourceAsarSha256);
|
|
418
485
|
addMismatch(issues, "ASAR header SHA-256", headerSha256,
|
|
419
|
-
patched ? recipe.patchedHeaderSha256 : recipe.sourceHeaderSha256);
|
|
486
|
+
patched ? recipe.patchedHeaderSha256 : predecessor ? expected?.headerSha256 : recipe.sourceHeaderSha256);
|
|
420
487
|
for (let index = 0; index < recipe.targets.length; index += 1) {
|
|
421
|
-
issues.push(...targetStateIssues(targets[index], recipe.targets[index],
|
|
488
|
+
issues.push(...targetStateIssues(targets[index], recipe.targets[index], state));
|
|
422
489
|
}
|
|
423
490
|
return issues;
|
|
424
491
|
};
|
|
425
492
|
|
|
426
493
|
const cleanIssues = issuesFor("clean");
|
|
427
494
|
const patchedIssues = issuesFor("patched");
|
|
428
|
-
const
|
|
495
|
+
const predecessorIssues = recipe.predecessor
|
|
496
|
+
? issuesFor("predecessor")
|
|
497
|
+
: ["recipe has no recognized predecessor patch"];
|
|
498
|
+
const legacyIssues = [...commonIssues];
|
|
499
|
+
if (!recipe.legacy) {
|
|
500
|
+
legacyIssues.push("recipe has no recognized legacy patch");
|
|
501
|
+
} else {
|
|
502
|
+
addMismatch(legacyIssues, "ASAR SHA-256", asarSha256, recipe.legacy.asarSha256);
|
|
503
|
+
addMismatch(legacyIssues, "ASAR header SHA-256", headerSha256, recipe.legacy.headerSha256);
|
|
504
|
+
}
|
|
505
|
+
const state = cleanIssues.length === 0
|
|
506
|
+
? "clean"
|
|
507
|
+
: patchedIssues.length === 0
|
|
508
|
+
? "patched"
|
|
509
|
+
: predecessorIssues.length === 0
|
|
510
|
+
? "predecessor"
|
|
511
|
+
: legacyIssues.length === 0 ? "legacy" : "drift";
|
|
429
512
|
return {
|
|
430
513
|
recipeId: recipe.id,
|
|
431
514
|
state,
|
|
@@ -435,19 +518,30 @@ export function inspectAsarBuffer(value, recipe = PM_STUDIO_2_9_7_RECIPE) {
|
|
|
435
518
|
targets,
|
|
436
519
|
cleanIssues,
|
|
437
520
|
patchedIssues,
|
|
521
|
+
predecessorIssues,
|
|
522
|
+
legacyIssues,
|
|
438
523
|
};
|
|
439
524
|
}
|
|
440
525
|
|
|
441
526
|
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
527
|
const before = inspectAsarBuffer(value, recipe);
|
|
446
528
|
if (before.state === "patched") {
|
|
447
529
|
return { changed: false, buffer: Buffer.from(value), before, after: before };
|
|
448
530
|
}
|
|
531
|
+
if (before.state === "predecessor") {
|
|
532
|
+
const error = new Error("Predecessor PM Studio split-origin patch must be migrated from a verified clean backup");
|
|
533
|
+
error.code = "PM_STUDIO_ASAR_PREDECESSOR";
|
|
534
|
+
error.inspection = before;
|
|
535
|
+
throw error;
|
|
536
|
+
}
|
|
537
|
+
if (before.state === "legacy") {
|
|
538
|
+
const error = new Error("Legacy PM Studio global-origin patch must be migrated from a verified clean backup");
|
|
539
|
+
error.code = "PM_STUDIO_ASAR_LEGACY";
|
|
540
|
+
error.inspection = before;
|
|
541
|
+
throw error;
|
|
542
|
+
}
|
|
449
543
|
if (before.state !== "clean") {
|
|
450
|
-
const error = new Error("PM Studio ASAR does not match the clean or
|
|
544
|
+
const error = new Error("PM Studio ASAR does not match the clean, current split-origin, predecessor, or legacy recipe");
|
|
451
545
|
error.code = "PM_STUDIO_ASAR_DRIFT";
|
|
452
546
|
error.inspection = before;
|
|
453
547
|
throw error;
|
|
@@ -455,16 +549,25 @@ export function patchAsarBuffer(value, recipe = PM_STUDIO_2_9_7_RECIPE) {
|
|
|
455
549
|
|
|
456
550
|
const output = Buffer.from(value);
|
|
457
551
|
const parsed = parseAsarBuffer(output);
|
|
458
|
-
const sourceSentinel = Buffer.from(recipe.sourceSentinel);
|
|
459
|
-
const patchedSentinel = Buffer.from(recipe.patchedSentinel);
|
|
460
552
|
|
|
461
553
|
for (const target of recipe.targets) {
|
|
462
554
|
const entry = findAsarEntry(parsed.header, target.path);
|
|
463
555
|
const offset = entryOffset(entry.offset, `${target.path} offset`);
|
|
464
|
-
const
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
556
|
+
const edit = target.edit;
|
|
557
|
+
if (!edit) throw new Error(`${target.path} has no split-origin edit recipe`);
|
|
558
|
+
const replacement = Buffer.from(edit.replacement);
|
|
559
|
+
if (replacement.length !== edit.length || sha256Hex(replacement) !== edit.patchedSha256) {
|
|
560
|
+
throw new Error(`${target.path} split-origin replacement does not match its recipe`);
|
|
561
|
+
}
|
|
562
|
+
const absoluteEditOffset = parsed.dataOffset + offset + edit.offset;
|
|
563
|
+
if (absoluteEditOffset !== edit.absoluteOffset) {
|
|
564
|
+
throw new Error(`${target.path} split-origin edit moved before patching`);
|
|
565
|
+
}
|
|
566
|
+
const existing = output.subarray(absoluteEditOffset, absoluteEditOffset + edit.length);
|
|
567
|
+
if (sha256Hex(existing) !== edit.sourceSha256) {
|
|
568
|
+
throw new Error(`${target.path} source edit window does not match its recipe`);
|
|
569
|
+
}
|
|
570
|
+
replacement.copy(output, absoluteEditOffset);
|
|
468
571
|
entry.integrity = {
|
|
469
572
|
...entry.integrity,
|
|
470
573
|
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
|
@@ -519,6 +519,10 @@ export function inspectPmStudioApp({
|
|
|
519
519
|
&& codeSign?.adHoc === true
|
|
520
520
|
&& (!recipe.patchedSigningMetadata
|
|
521
521
|
|| JSON.stringify(signingMetadata(codeSign)) === JSON.stringify(recipe.patchedSigningMetadata));
|
|
522
|
+
const predecessorSignatureAccepted = codeSign?.valid === true
|
|
523
|
+
&& codeSign?.adHoc === true
|
|
524
|
+
&& Boolean(recipe.predecessor?.signingMetadata)
|
|
525
|
+
&& JSON.stringify(signingMetadata(codeSign)) === JSON.stringify(recipe.predecessor.signingMetadata);
|
|
522
526
|
const clean = issues.length === 0
|
|
523
527
|
&& asar.state === "clean"
|
|
524
528
|
&& integrityMatches(plistIntegrity, "SHA256", recipe.sourceHeaderSha256)
|
|
@@ -530,8 +534,16 @@ export function inspectPmStudioApp({
|
|
|
530
534
|
&& asar.state === "patched"
|
|
531
535
|
&& integrityMatches(plistIntegrity, "SHA256", recipe.patchedHeaderSha256)
|
|
532
536
|
&& patchedSignatureAccepted;
|
|
537
|
+
const predecessor = issues.length === 0
|
|
538
|
+
&& asar.state === "predecessor"
|
|
539
|
+
&& integrityMatches(plistIntegrity, "SHA256", recipe.predecessor?.headerSha256)
|
|
540
|
+
&& predecessorSignatureAccepted;
|
|
541
|
+
const legacy = issues.length === 0
|
|
542
|
+
&& asar.state === "legacy"
|
|
543
|
+
&& integrityMatches(plistIntegrity, "SHA256", recipe.legacy?.headerSha256)
|
|
544
|
+
&& patchedSignatureAccepted;
|
|
533
545
|
|
|
534
|
-
if (!clean && !patched) {
|
|
546
|
+
if (!clean && !patched && !predecessor && !legacy) {
|
|
535
547
|
if (asar.state === "drift") issues.push("app.asar is unknown drift");
|
|
536
548
|
else if (asar.state === "clean"
|
|
537
549
|
&& !integrityMatches(plistIntegrity, "SHA256", recipe.sourceHeaderSha256)) {
|
|
@@ -539,6 +551,12 @@ export function inspectPmStudioApp({
|
|
|
539
551
|
} else if (asar.state === "patched"
|
|
540
552
|
&& !integrityMatches(plistIntegrity, "SHA256", recipe.patchedHeaderSha256)) {
|
|
541
553
|
issues.push("patched ASAR does not match ElectronAsarIntegrity");
|
|
554
|
+
} else if (asar.state === "predecessor"
|
|
555
|
+
&& !integrityMatches(plistIntegrity, "SHA256", recipe.predecessor?.headerSha256)) {
|
|
556
|
+
issues.push("predecessor ASAR does not match ElectronAsarIntegrity");
|
|
557
|
+
} else if (asar.state === "legacy"
|
|
558
|
+
&& !integrityMatches(plistIntegrity, "SHA256", recipe.legacy?.headerSha256)) {
|
|
559
|
+
issues.push("legacy ASAR does not match ElectronAsarIntegrity");
|
|
542
560
|
}
|
|
543
561
|
if (asar.state === "clean" && recipe.sourceBundleContent && !exactSourceBundle) {
|
|
544
562
|
issues.push("bundle content does not match the exact clean recipe");
|
|
@@ -547,7 +565,9 @@ export function inspectPmStudioApp({
|
|
|
547
565
|
issues.push("codesign verification failed");
|
|
548
566
|
}
|
|
549
567
|
else if (asar.state === "clean" && codeSign.adHoc) issues.push("clean bundle has an unexpected ad-hoc signature");
|
|
550
|
-
else if (
|
|
568
|
+
else if (["patched", "predecessor", "legacy"].includes(asar.state) && !codeSign.adHoc) {
|
|
569
|
+
issues.push(`${asar.state} bundle is not ad-hoc signed`);
|
|
570
|
+
}
|
|
551
571
|
else if (asar.state === "clean" && codeSign.teamIdentifier !== recipe.sourceTeamIdentifier) {
|
|
552
572
|
issues.push(`clean bundle TeamIdentifier is ${codeSign.teamIdentifier || "missing"}, expected ${recipe.sourceTeamIdentifier}`);
|
|
553
573
|
}
|
|
@@ -559,15 +579,21 @@ export function inspectPmStudioApp({
|
|
|
559
579
|
&& executableIntegrity.frameworkSha256 !== recipe.sourceElectronFrameworkSha256) {
|
|
560
580
|
issues.push("Electron Framework hash does not match the clean recipe");
|
|
561
581
|
}
|
|
562
|
-
if (asar.state
|
|
582
|
+
if (["patched", "legacy"].includes(asar.state) && codeSign?.valid && codeSign?.adHoc
|
|
563
583
|
&& recipe.patchedSigningMetadata
|
|
564
584
|
&& !patchedSignatureAccepted) {
|
|
565
|
-
issues.push(
|
|
585
|
+
issues.push(`${asar.state} bundle signing metadata does not match the recipe`);
|
|
586
|
+
}
|
|
587
|
+
if (asar.state === "predecessor" && codeSign?.valid && codeSign?.adHoc
|
|
588
|
+
&& !predecessorSignatureAccepted) {
|
|
589
|
+
issues.push("predecessor bundle signing metadata does not match the recipe");
|
|
566
590
|
}
|
|
567
591
|
}
|
|
568
592
|
|
|
569
593
|
return {
|
|
570
|
-
state: clean
|
|
594
|
+
state: clean
|
|
595
|
+
? "clean"
|
|
596
|
+
: patched ? "patched" : predecessor ? "predecessor" : legacy ? "legacy" : "drift",
|
|
571
597
|
metadata: {
|
|
572
598
|
version: String(metadata.version),
|
|
573
599
|
build: String(metadata.build),
|
|
@@ -676,6 +702,28 @@ function backupManifest(recipe, createdAt) {
|
|
|
676
702
|
};
|
|
677
703
|
}
|
|
678
704
|
|
|
705
|
+
function patchRecordMatches(record, recipe, state) {
|
|
706
|
+
const expected = state === "legacy"
|
|
707
|
+
? recipe.legacy
|
|
708
|
+
: state === "predecessor"
|
|
709
|
+
? recipe.predecessor
|
|
710
|
+
: { asarSha256: recipe.patchedAsarSha256, headerSha256: recipe.patchedHeaderSha256 };
|
|
711
|
+
return Boolean(expected)
|
|
712
|
+
&& record?.asar_sha256 === expected.asarSha256
|
|
713
|
+
&& record?.asar_header_sha256 === expected.headerSha256
|
|
714
|
+
&& integrityMatches(record?.electron_asar_integrity, "SHA256", expected.headerSha256);
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
function patchRecordForState(manifest, recipe, state) {
|
|
718
|
+
const historicalKey = state === "legacy"
|
|
719
|
+
? "legacy_patched"
|
|
720
|
+
: state === "predecessor" ? "predecessor_patched" : "";
|
|
721
|
+
if (historicalKey && patchRecordMatches(manifest?.[historicalKey], recipe, state)) {
|
|
722
|
+
return manifest[historicalKey];
|
|
723
|
+
}
|
|
724
|
+
return patchRecordMatches(manifest?.patched, recipe, state) ? manifest.patched : null;
|
|
725
|
+
}
|
|
726
|
+
|
|
679
727
|
function validateBackup({ backupDir, recipe, operations }) {
|
|
680
728
|
const manifestPath = path.join(backupDir, "manifest.json");
|
|
681
729
|
const backupAppPath = path.join(backupDir, "PM Studio.app");
|
|
@@ -702,9 +750,11 @@ function validateBackup({ backupDir, recipe, operations }) {
|
|
|
702
750
|
&& JSON.stringify(manifest?.source?.bundle_content) !== JSON.stringify(recipe.sourceBundleContent))
|
|
703
751
|
|| (recipe.sourceArtifact
|
|
704
752
|
&& JSON.stringify(manifest?.source?.artifact) !== JSON.stringify(recipe.sourceArtifact))
|
|
705
|
-
|| manifest?.
|
|
706
|
-
|
|
707
|
-
|| !
|
|
753
|
+
|| (manifest?.predecessor_patched !== undefined
|
|
754
|
+
&& !patchRecordMatches(manifest.predecessor_patched, recipe, "predecessor"))
|
|
755
|
+
|| (!patchRecordForState(manifest, recipe, "patched")
|
|
756
|
+
&& !patchRecordForState(manifest, recipe, "legacy")
|
|
757
|
+
&& !patchRecordForState(manifest, recipe, "predecessor"))) {
|
|
708
758
|
throw setupError("PM_STUDIO_BACKUP_INVALID", `Existing PM Studio backup does not match recipe ${recipe.id}`);
|
|
709
759
|
}
|
|
710
760
|
const inspection = inspectPmStudioApp({ appPath: backupAppPath, recipe, operations });
|
|
@@ -750,12 +800,29 @@ function ensureBackup({ appPath, backupRoot, recipe, operations }) {
|
|
|
750
800
|
|
|
751
801
|
function writePatchedBinaryRecord({ backup, inspection, operations, recipe }) {
|
|
752
802
|
const manifest = JSON.parse(fs.readFileSync(backup.manifestPath, "utf8"));
|
|
753
|
-
manifest.
|
|
754
|
-
|
|
755
|
-
|
|
803
|
+
if (manifest.predecessor_patched !== undefined
|
|
804
|
+
&& !patchRecordMatches(manifest.predecessor_patched, recipe, "predecessor")) {
|
|
805
|
+
throw setupError("PM_STUDIO_BACKUP_INVALID", "Existing PM Studio predecessor record does not match the patch recipe");
|
|
806
|
+
}
|
|
807
|
+
if (manifest.predecessor_patched === undefined
|
|
808
|
+
&& patchRecordMatches(manifest.patched, recipe, "predecessor")) {
|
|
809
|
+
manifest.predecessor_patched = manifest.patched;
|
|
810
|
+
}
|
|
811
|
+
if (!patchRecordMatches(manifest.legacy_patched, recipe, "legacy")
|
|
812
|
+
&& patchRecordMatches(manifest.patched, recipe, "legacy")) {
|
|
813
|
+
manifest.legacy_patched = manifest.patched;
|
|
814
|
+
}
|
|
815
|
+
manifest.patched = {
|
|
816
|
+
asar_sha256: recipe.patchedAsarSha256,
|
|
817
|
+
asar_header_sha256: recipe.patchedHeaderSha256,
|
|
818
|
+
electron_asar_integrity: { algorithm: "SHA256", hash: recipe.patchedHeaderSha256 },
|
|
819
|
+
binaries: {
|
|
820
|
+
main_executable_sha256: inspection.executableIntegrity.executableSha256,
|
|
821
|
+
electron_framework_sha256: inspection.executableIntegrity.frameworkSha256,
|
|
822
|
+
},
|
|
823
|
+
signing_metadata: signingMetadata(inspection.codeSign),
|
|
824
|
+
...(recipe.sourceBundleContent ? { bundle_content: inspection.bundleContent } : {}),
|
|
756
825
|
};
|
|
757
|
-
manifest.patched.signing_metadata = signingMetadata(inspection.codeSign);
|
|
758
|
-
if (recipe.sourceBundleContent) manifest.patched.bundle_content = inspection.bundleContent;
|
|
759
826
|
const temporaryPath = `${backup.manifestPath}.${operations.uuid()}.tmp`;
|
|
760
827
|
try {
|
|
761
828
|
fs.writeFileSync(temporaryPath, `${JSON.stringify(manifest, null, 2)}\n`, { mode: 0o600 });
|
|
@@ -766,7 +833,14 @@ function writePatchedBinaryRecord({ backup, inspection, operations, recipe }) {
|
|
|
766
833
|
backup.manifest = manifest;
|
|
767
834
|
}
|
|
768
835
|
|
|
769
|
-
export function assertPatchedBinaryRecord({
|
|
836
|
+
export function assertPatchedBinaryRecord({
|
|
837
|
+
inspection,
|
|
838
|
+
manifestPath,
|
|
839
|
+
recipe,
|
|
840
|
+
recordState = ["legacy", "predecessor"].includes(inspection?.state)
|
|
841
|
+
? inspection.state
|
|
842
|
+
: "patched",
|
|
843
|
+
}) {
|
|
770
844
|
let manifest;
|
|
771
845
|
try {
|
|
772
846
|
manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8"));
|
|
@@ -790,23 +864,24 @@ export function assertPatchedBinaryRecord({ inspection, manifestPath, recipe })
|
|
|
790
864
|
|| JSON.stringify(manifest?.source?.bundle_content) !== JSON.stringify(recipe.sourceBundleContent)
|
|
791
865
|
))
|
|
792
866
|
|| (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))) {
|
|
867
|
+
&& JSON.stringify(manifest?.source?.artifact) !== JSON.stringify(recipe.sourceArtifact)))) {
|
|
797
868
|
throw setupError("PM_STUDIO_PATCH_RECORD_INVALID", "PM Studio patched-binary record does not match the installed patch recipe");
|
|
798
869
|
}
|
|
799
|
-
const
|
|
870
|
+
const record = patchRecordForState(manifest, recipe, recordState);
|
|
871
|
+
if (!record) {
|
|
872
|
+
throw setupError("PM_STUDIO_PATCH_RECORD_INVALID", `PM Studio ${recordState} record does not match the installed patch recipe`);
|
|
873
|
+
}
|
|
874
|
+
const binaries = record.binaries;
|
|
800
875
|
if (!binaries
|
|
801
876
|
|| binaries.main_executable_sha256 !== inspection.executableIntegrity.executableSha256
|
|
802
877
|
|| binaries.electron_framework_sha256 !== inspection.executableIntegrity.frameworkSha256) {
|
|
803
878
|
throw setupError("PM_STUDIO_BUNDLE_DRIFT", "PM Studio executable or Electron Framework differs from the installed patch record");
|
|
804
879
|
}
|
|
805
|
-
if (JSON.stringify(
|
|
880
|
+
if (JSON.stringify(record.signing_metadata) !== JSON.stringify(signingMetadata(inspection.codeSign))) {
|
|
806
881
|
throw setupError("PM_STUDIO_BUNDLE_DRIFT", "PM Studio signing metadata differs from the installed patch record");
|
|
807
882
|
}
|
|
808
883
|
if (recipe?.sourceBundleContent
|
|
809
|
-
&& JSON.stringify(
|
|
884
|
+
&& JSON.stringify(record.bundle_content) !== JSON.stringify(inspection.bundleContent)) {
|
|
810
885
|
throw setupError("PM_STUDIO_BUNDLE_DRIFT", "PM Studio bundle content differs from the installed patch record");
|
|
811
886
|
}
|
|
812
887
|
}
|
|
@@ -922,12 +997,15 @@ export async function runPmStudioSetup({
|
|
|
922
997
|
emit(`[OK] PM Studio ${recipe.version} build ${recipe.build} is already patched; no backup or signing was repeated.`);
|
|
923
998
|
emit(`[OK] Verified backup: ${backup.backupAppPath}`);
|
|
924
999
|
emit(`[OK] Backup manifest: ${backup.manifestPath}`);
|
|
925
|
-
emit(`[INFO] Start ${commandName} before
|
|
1000
|
+
emit(`[INFO] Start ${commandName} before using Claude in PM Studio; GPT remains on PM Studio's native GitHub Copilot path.`);
|
|
926
1001
|
return { status: "already_patched", changed: false, recipe: recipe.id, backup, inspection: source, messages };
|
|
927
1002
|
}
|
|
928
|
-
|
|
1003
|
+
const migratingLegacy = source.state === "legacy";
|
|
1004
|
+
const migratingPredecessor = source.state === "predecessor";
|
|
1005
|
+
const migratingExistingPatch = migratingLegacy || migratingPredecessor;
|
|
1006
|
+
if (source.state !== "clean" && !migratingExistingPatch) {
|
|
929
1007
|
throw setupError("PM_STUDIO_BUNDLE_DRIFT",
|
|
930
|
-
`PM Studio ${recipe.version} build ${recipe.build} does not match the clean or
|
|
1008
|
+
`PM Studio ${recipe.version} build ${recipe.build} does not match the clean, current split-origin, predecessor, or legacy recipe; no files were changed`, source.issues);
|
|
931
1009
|
}
|
|
932
1010
|
|
|
933
1011
|
if (source.sourceVerification === "exact-bundle-content") {
|
|
@@ -935,17 +1013,38 @@ export async function runPmStudioSetup({
|
|
|
935
1013
|
}
|
|
936
1014
|
|
|
937
1015
|
const backupExists = fs.existsSync(finalBackupDir);
|
|
1016
|
+
let verifiedBackup = null;
|
|
1017
|
+
if (migratingExistingPatch) {
|
|
1018
|
+
if (!backupExists) {
|
|
1019
|
+
throw setupError("PM_STUDIO_BACKUP_INVALID",
|
|
1020
|
+
`${migratingLegacy ? "Legacy global-origin" : "Predecessor split-origin"} PM Studio patch has no matching verified clean backup; no files were changed`);
|
|
1021
|
+
}
|
|
1022
|
+
verifiedBackup = validateBackup({ backupDir: finalBackupDir, recipe, operations });
|
|
1023
|
+
assertPatchedBinaryRecord({
|
|
1024
|
+
inspection: source,
|
|
1025
|
+
manifestPath: verifiedBackup.manifestPath,
|
|
1026
|
+
recipe,
|
|
1027
|
+
recordState: source.state,
|
|
1028
|
+
});
|
|
1029
|
+
emit(migratingLegacy
|
|
1030
|
+
? "[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."
|
|
1031
|
+
: "[WARN] Predecessor split-origin patch detected; setup will rebuild from the verified clean backup so delayed Claude model discovery no longer falls back to the native catalog.");
|
|
1032
|
+
}
|
|
938
1033
|
assertSpace({ appPath, backupRoot, backupExists, operations });
|
|
939
1034
|
|
|
940
1035
|
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 });
|
|
1036
|
+
const backup = verifiedBackup || ensureBackup({ appPath, backupRoot, recipe, operations });
|
|
942
1037
|
const stagePath = path.join(path.dirname(appPath), `.${path.basename(appPath)}.ccdx-stage-${operations.uuid()}`);
|
|
943
1038
|
let preserveStage = false;
|
|
944
1039
|
try {
|
|
945
|
-
operations.copyBundle({
|
|
1040
|
+
operations.copyBundle({
|
|
1041
|
+
source: migratingExistingPatch ? backup.backupAppPath : appPath,
|
|
1042
|
+
destination: stagePath,
|
|
1043
|
+
processRunner: operations.processRunner,
|
|
1044
|
+
});
|
|
946
1045
|
const stagedSource = inspectPmStudioApp({ appPath: stagePath, recipe, operations });
|
|
947
1046
|
if (stagedSource.state !== "clean"
|
|
948
|
-
|| (recipe.sourceBundleContent
|
|
1047
|
+
|| (!migratingExistingPatch && recipe.sourceBundleContent
|
|
949
1048
|
&& stagedSource.bundleContent?.sha256 !== source.bundleContent?.sha256)) {
|
|
950
1049
|
throw setupError("PM_STUDIO_STAGE_INVALID", "PM Studio staging copy does not match the exact clean source bundle", stagedSource.issues);
|
|
951
1050
|
}
|
|
@@ -966,12 +1065,12 @@ export async function runPmStudioSetup({
|
|
|
966
1065
|
if (staged.state !== "patched") {
|
|
967
1066
|
throw setupError("PM_STUDIO_STAGE_VERIFY_FAILED", "Patched PM Studio staging bundle failed verification", staged.issues);
|
|
968
1067
|
}
|
|
969
|
-
assertSigningMetadataPreserved(
|
|
1068
|
+
assertSigningMetadataPreserved(stagedSource, staged, recipe);
|
|
970
1069
|
writePatchedBinaryRecord({ backup, inspection: staged, operations, recipe });
|
|
971
1070
|
|
|
972
1071
|
assertNoBlockingProcesses(operations, appPath);
|
|
973
1072
|
const unchangedSource = inspectPmStudioApp({ appPath, recipe, operations });
|
|
974
|
-
if (unchangedSource.state !==
|
|
1073
|
+
if (unchangedSource.state !== source.state
|
|
975
1074
|
|| unchangedSource.asar.asarSha256 !== source.asar.asarSha256
|
|
976
1075
|
|| unchangedSource.asar.headerSha256 !== source.asar.headerSha256
|
|
977
1076
|
|| (recipe.sourceBundleContent
|
|
@@ -989,12 +1088,17 @@ export async function runPmStudioSetup({
|
|
|
989
1088
|
`PM Studio replacement final state could not be verified. Do not launch it. ${recoveryMessage(backup)}${preserveStage ? ` Verified staging retained: ${stagePath}.` : ""}`);
|
|
990
1089
|
}
|
|
991
1090
|
if (installed.state !== "patched") {
|
|
992
|
-
|
|
993
|
-
|
|
1091
|
+
const originalRetained = installed.state === source.state
|
|
1092
|
+
&& installed.asar.asarSha256 === source.asar.asarSha256
|
|
1093
|
+
&& installed.asar.headerSha256 === source.asar.headerSha256
|
|
1094
|
+
&& (!recipe.sourceBundleContent
|
|
1095
|
+
|| installed.bundleContent?.sha256 === source.bundleContent?.sha256);
|
|
1096
|
+
preserveStage = !originalRetained && fs.existsSync(stagePath);
|
|
1097
|
+
const code = originalRetained
|
|
994
1098
|
? "PM_STUDIO_REPLACE_FAILED"
|
|
995
1099
|
: "PM_STUDIO_INSTALL_VERIFY_FAILED";
|
|
996
|
-
const stateMessage =
|
|
997
|
-
?
|
|
1100
|
+
const stateMessage = originalRetained
|
|
1101
|
+
? `The installed PM Studio remains the verified ${source.state} bundle`
|
|
998
1102
|
: "The installed PM Studio did not retain the verified patched state; do not launch it";
|
|
999
1103
|
throw setupError(code,
|
|
1000
1104
|
`${stateMessage}. ${recoveryMessage(backup)}${preserveStage ? ` Verified staging retained: ${stagePath}.` : ""}`,
|
|
@@ -1011,7 +1115,11 @@ export async function runPmStudioSetup({
|
|
|
1011
1115
|
emit("[WARN] Atomic replacement reported an error after the exact patched bundle became installed; the final bundle was independently verified.");
|
|
1012
1116
|
}
|
|
1013
1117
|
|
|
1014
|
-
emit(
|
|
1118
|
+
emit(migratingLegacy
|
|
1119
|
+
? `[OK] Migrated PM Studio ${recipe.version} build ${recipe.build} from the legacy global-origin patch to the split-origin patch.`
|
|
1120
|
+
: migratingPredecessor
|
|
1121
|
+
? `[OK] Migrated PM Studio ${recipe.version} build ${recipe.build} from the predecessor split-origin patch to the current split-origin patch.`
|
|
1122
|
+
: `[OK] Patched PM Studio ${recipe.version} build ${recipe.build}.`);
|
|
1015
1123
|
emit(`[OK] app.asar SHA-256: ${installed.asar.asarSha256}`);
|
|
1016
1124
|
emit(`[OK] Verified backup: ${backup.backupAppPath}${backup.reused ? " (reused)" : ""}`);
|
|
1017
1125
|
emit(`[OK] Backup manifest: ${backup.manifestPath}`);
|
|
@@ -1019,9 +1127,9 @@ export async function runPmStudioSetup({
|
|
|
1019
1127
|
emit(`[INFO] Restore step 1/3: quit PM Studio and its updater.`);
|
|
1020
1128
|
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
1129
|
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
|
|
1130
|
+
emit(`[INFO] Start ${commandName} before using Claude in PM Studio; GPT remains on PM Studio's native GitHub Copilot path.`);
|
|
1023
1131
|
return {
|
|
1024
|
-
status: "patched",
|
|
1132
|
+
status: migratingExistingPatch ? "migrated" : "patched",
|
|
1025
1133
|
changed: true,
|
|
1026
1134
|
recipe: recipe.id,
|
|
1027
1135
|
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;
|
|
@@ -125,10 +170,15 @@ export async function inspectPmStudioStatus({
|
|
|
125
170
|
const recipe = selectRecipe(normalized, recipes);
|
|
126
171
|
if (recipe) {
|
|
127
172
|
app = { ...inspectApp({ appPath, recipe, operations }), recipe: recipe.id };
|
|
128
|
-
if (app.state
|
|
173
|
+
if (["patched", "predecessor"].includes(app.state)) {
|
|
129
174
|
const manifestPath = pmStudioPatchManifestPath({ home, backupRoot, recipe });
|
|
130
175
|
try {
|
|
131
|
-
verifyPatchRecord({
|
|
176
|
+
verifyPatchRecord({
|
|
177
|
+
inspection: app,
|
|
178
|
+
manifestPath,
|
|
179
|
+
recipe,
|
|
180
|
+
recordState: app.state,
|
|
181
|
+
});
|
|
132
182
|
app.patchRecord = { valid: true, manifestPath, reason: "" };
|
|
133
183
|
} catch (error) {
|
|
134
184
|
const reason = safeMessage(error);
|
|
@@ -163,6 +213,18 @@ export async function inspectPmStudioStatus({
|
|
|
163
213
|
} catch (error) {
|
|
164
214
|
runtime = { ok: false, baseUrl: adapter.baseUrl, data: null, issues: [safeMessage(error)] };
|
|
165
215
|
}
|
|
216
|
+
if (runtime.data) {
|
|
217
|
+
try {
|
|
218
|
+
runtime = inspectRelayProbe(runtime, await probePmStudioRelayFn({ baseUrl: adapter.baseUrl }));
|
|
219
|
+
} catch (error) {
|
|
220
|
+
runtime = {
|
|
221
|
+
...runtime,
|
|
222
|
+
ok: false,
|
|
223
|
+
relayProbe: null,
|
|
224
|
+
issues: [...runtime.issues, `PM Studio relay probe failed: ${safeMessage(error)}`],
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
}
|
|
166
228
|
}
|
|
167
229
|
const operational = app.state === "patched"
|
|
168
230
|
&& app.patchRecord?.valid === true
|
|
@@ -182,6 +244,12 @@ function appStatusItem(result, commandName) {
|
|
|
182
244
|
const reason = app.patchRecord?.reason || "installed patch record is missing";
|
|
183
245
|
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
246
|
}
|
|
247
|
+
if (app.state === "predecessor" && app.patchRecord?.valid === true) {
|
|
248
|
+
return { kind: "warn", component: "App patch", detail: `${version}; verified predecessor split patch`, message: `PM Studio ${version} has a verified predecessor split patch; run ${commandName} pms setup to migrate` };
|
|
249
|
+
}
|
|
250
|
+
if (app.state === "legacy") {
|
|
251
|
+
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` };
|
|
252
|
+
}
|
|
185
253
|
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
254
|
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
255
|
if (app.state === "drift" && app.patchRecipeMatched) {
|
|
@@ -203,7 +271,7 @@ function pmStudioStatusItems(result, commandName) {
|
|
|
203
271
|
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
272
|
}
|
|
205
273
|
if (result.adapter?.ok && result.runtime?.ok) {
|
|
206
|
-
items.push({ kind: "ok", component: "PM relay", detail: `verified at ${result.adapter.baseUrl}`, message: `PM
|
|
274
|
+
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
275
|
} else if (result.adapter?.ok) {
|
|
208
276
|
const reason = result.runtime?.issues?.join("; ") || "runtime status is unavailable";
|
|
209
277
|
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 +281,9 @@ function pmStudioStatusItems(result, commandName) {
|
|
|
213
281
|
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
282
|
}
|
|
215
283
|
if (result.runtime?.ok) {
|
|
216
|
-
items.push({ kind: "info", component: "Routing", detail: "GPT ->
|
|
284
|
+
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
285
|
} else {
|
|
218
|
-
items.push({ kind: "info", component: "Routing", detail: "expected: GPT ->
|
|
286
|
+
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
287
|
}
|
|
220
288
|
return items;
|
|
221
289
|
}
|
|
@@ -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() {
|